From 0867e401bc6ca48bfc0854e581c9b11cdabfe6c7 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar Date: Mon, 2 Mar 2026 13:30:17 +0100 Subject: [PATCH] Initial commit: Compliance Scanner Agent Autonomous security and compliance scanning agent for git repositories. Features: SAST (Semgrep), SBOM (Syft), CVE monitoring (OSV.dev/NVD), GDPR/OAuth pattern detection, LLM triage, issue creation (GitHub/GitLab/Jira), PR reviews, and Dioxus fullstack dashboard. Co-Authored-By: Claude Opus 4.6 --- .env.example | 39 + .gitignore | 6 + Cargo.lock | 5308 +++++++++++++++++ Cargo.toml | 24 + Dioxus.toml | 11 + Dockerfile.agent | 14 + Dockerfile.dashboard | 18 + README.md | 205 + assets/favicon.svg | 28 + assets/main.css | 315 + assets/tailwind.css | 1 + bin/main.rs | 23 + build.rs | 24 + clippy.toml | 1 + compliance-agent/Cargo.toml | 35 + compliance-agent/src/agent.rs | 45 + compliance-agent/src/api/handlers/mod.rs | 334 ++ compliance-agent/src/api/mod.rs | 5 + compliance-agent/src/api/routes.rs | 19 + compliance-agent/src/api/server.rs | 28 + compliance-agent/src/config.rs | 43 + compliance-agent/src/database.rs | 122 + compliance-agent/src/error.rs | 41 + compliance-agent/src/llm/client.rs | 157 + compliance-agent/src/llm/descriptions.rs | 65 + compliance-agent/src/llm/fixes.rs | 27 + compliance-agent/src/llm/mod.rs | 10 + compliance-agent/src/llm/pr_review.rs | 77 + compliance-agent/src/llm/triage.rs | 73 + compliance-agent/src/main.rs | 53 + compliance-agent/src/pipeline/cve.rs | 199 + compliance-agent/src/pipeline/dedup.rs | 10 + compliance-agent/src/pipeline/git.rs | 100 + compliance-agent/src/pipeline/mod.rs | 7 + compliance-agent/src/pipeline/orchestrator.rs | 252 + compliance-agent/src/pipeline/patterns.rs | 226 + compliance-agent/src/pipeline/sbom.rs | 186 + compliance-agent/src/pipeline/semgrep.rs | 110 + compliance-agent/src/scheduler.rs | 105 + compliance-agent/src/trackers/github.rs | 161 + compliance-agent/src/trackers/gitlab.rs | 201 + compliance-agent/src/trackers/jira.rs | 231 + compliance-agent/src/trackers/mod.rs | 3 + compliance-agent/src/webhooks/github.rs | 130 + compliance-agent/src/webhooks/gitlab.rs | 95 + compliance-agent/src/webhooks/mod.rs | 5 + compliance-agent/src/webhooks/server.rs | 27 + compliance-core/Cargo.toml | 18 + compliance-core/src/config.rs | 34 + compliance-core/src/error.rs | 41 + compliance-core/src/lib.rs | 7 + compliance-core/src/models/cve.rs | 46 + compliance-core/src/models/finding.rs | 115 + compliance-core/src/models/issue.rs | 77 + compliance-core/src/models/mod.rs | 13 + compliance-core/src/models/repository.rs | 53 + compliance-core/src/models/sbom.rs | 43 + compliance-core/src/models/scan.rs | 81 + compliance-core/src/traits/issue_tracker.rs | 55 + compliance-core/src/traits/mod.rs | 5 + compliance-core/src/traits/scanner.rs | 17 + compliance-dashboard/Cargo.toml | 54 + compliance-dashboard/assets/favicon.svg | 28 + compliance-dashboard/assets/main.css | 315 + compliance-dashboard/assets/tailwind.css | 1 + compliance-dashboard/src/app.rs | 38 + .../src/components/app_shell.rs | 16 + .../src/components/code_snippet.rs | 23 + compliance-dashboard/src/components/mod.rs | 7 + .../src/components/page_header.rs | 13 + .../src/components/pagination.rs | 33 + .../src/components/severity_badge.rs | 16 + .../src/components/sidebar.rs | 81 + .../src/components/stat_card.rs | 21 + .../src/infrastructure/config.rs | 18 + .../src/infrastructure/database.rs | 45 + .../src/infrastructure/error.rs | 26 + .../src/infrastructure/findings.rs | 71 + .../src/infrastructure/issues.rs | 22 + .../src/infrastructure/mod.rs | 13 + .../src/infrastructure/repositories.rs | 64 + .../src/infrastructure/sbom.rs | 22 + .../src/infrastructure/scans.rs | 22 + .../src/infrastructure/server.rs | 41 + .../src/infrastructure/server_state.rs | 46 + .../src/infrastructure/stats.rs | 27 + compliance-dashboard/src/lib.rs | 8 + .../src/pages/finding_detail.rs | 117 + compliance-dashboard/src/pages/findings.rs | 124 + compliance-dashboard/src/pages/issues.rs | 87 + compliance-dashboard/src/pages/mod.rs | 15 + compliance-dashboard/src/pages/overview.rs | 104 + .../src/pages/repositories.rs | 155 + compliance-dashboard/src/pages/sbom.rs | 85 + compliance-dashboard/src/pages/settings.rs | 142 + docker-compose.yml | 45 + styles/input.css | 1 + 97 files changed, 11750 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dioxus.toml create mode 100644 Dockerfile.agent create mode 100644 Dockerfile.dashboard create mode 100644 README.md create mode 100644 assets/favicon.svg create mode 100644 assets/main.css create mode 100644 assets/tailwind.css create mode 100644 bin/main.rs create mode 100644 build.rs create mode 100644 clippy.toml create mode 100644 compliance-agent/Cargo.toml create mode 100644 compliance-agent/src/agent.rs create mode 100644 compliance-agent/src/api/handlers/mod.rs create mode 100644 compliance-agent/src/api/mod.rs create mode 100644 compliance-agent/src/api/routes.rs create mode 100644 compliance-agent/src/api/server.rs create mode 100644 compliance-agent/src/config.rs create mode 100644 compliance-agent/src/database.rs create mode 100644 compliance-agent/src/error.rs create mode 100644 compliance-agent/src/llm/client.rs create mode 100644 compliance-agent/src/llm/descriptions.rs create mode 100644 compliance-agent/src/llm/fixes.rs create mode 100644 compliance-agent/src/llm/mod.rs create mode 100644 compliance-agent/src/llm/pr_review.rs create mode 100644 compliance-agent/src/llm/triage.rs create mode 100644 compliance-agent/src/main.rs create mode 100644 compliance-agent/src/pipeline/cve.rs create mode 100644 compliance-agent/src/pipeline/dedup.rs create mode 100644 compliance-agent/src/pipeline/git.rs create mode 100644 compliance-agent/src/pipeline/mod.rs create mode 100644 compliance-agent/src/pipeline/orchestrator.rs create mode 100644 compliance-agent/src/pipeline/patterns.rs create mode 100644 compliance-agent/src/pipeline/sbom.rs create mode 100644 compliance-agent/src/pipeline/semgrep.rs create mode 100644 compliance-agent/src/scheduler.rs create mode 100644 compliance-agent/src/trackers/github.rs create mode 100644 compliance-agent/src/trackers/gitlab.rs create mode 100644 compliance-agent/src/trackers/jira.rs create mode 100644 compliance-agent/src/trackers/mod.rs create mode 100644 compliance-agent/src/webhooks/github.rs create mode 100644 compliance-agent/src/webhooks/gitlab.rs create mode 100644 compliance-agent/src/webhooks/mod.rs create mode 100644 compliance-agent/src/webhooks/server.rs create mode 100644 compliance-core/Cargo.toml create mode 100644 compliance-core/src/config.rs create mode 100644 compliance-core/src/error.rs create mode 100644 compliance-core/src/lib.rs create mode 100644 compliance-core/src/models/cve.rs create mode 100644 compliance-core/src/models/finding.rs create mode 100644 compliance-core/src/models/issue.rs create mode 100644 compliance-core/src/models/mod.rs create mode 100644 compliance-core/src/models/repository.rs create mode 100644 compliance-core/src/models/sbom.rs create mode 100644 compliance-core/src/models/scan.rs create mode 100644 compliance-core/src/traits/issue_tracker.rs create mode 100644 compliance-core/src/traits/mod.rs create mode 100644 compliance-core/src/traits/scanner.rs create mode 100644 compliance-dashboard/Cargo.toml create mode 100644 compliance-dashboard/assets/favicon.svg create mode 100644 compliance-dashboard/assets/main.css create mode 100644 compliance-dashboard/assets/tailwind.css create mode 100644 compliance-dashboard/src/app.rs create mode 100644 compliance-dashboard/src/components/app_shell.rs create mode 100644 compliance-dashboard/src/components/code_snippet.rs create mode 100644 compliance-dashboard/src/components/mod.rs create mode 100644 compliance-dashboard/src/components/page_header.rs create mode 100644 compliance-dashboard/src/components/pagination.rs create mode 100644 compliance-dashboard/src/components/severity_badge.rs create mode 100644 compliance-dashboard/src/components/sidebar.rs create mode 100644 compliance-dashboard/src/components/stat_card.rs create mode 100644 compliance-dashboard/src/infrastructure/config.rs create mode 100644 compliance-dashboard/src/infrastructure/database.rs create mode 100644 compliance-dashboard/src/infrastructure/error.rs create mode 100644 compliance-dashboard/src/infrastructure/findings.rs create mode 100644 compliance-dashboard/src/infrastructure/issues.rs create mode 100644 compliance-dashboard/src/infrastructure/mod.rs create mode 100644 compliance-dashboard/src/infrastructure/repositories.rs create mode 100644 compliance-dashboard/src/infrastructure/sbom.rs create mode 100644 compliance-dashboard/src/infrastructure/scans.rs create mode 100644 compliance-dashboard/src/infrastructure/server.rs create mode 100644 compliance-dashboard/src/infrastructure/server_state.rs create mode 100644 compliance-dashboard/src/infrastructure/stats.rs create mode 100644 compliance-dashboard/src/lib.rs create mode 100644 compliance-dashboard/src/pages/finding_detail.rs create mode 100644 compliance-dashboard/src/pages/findings.rs create mode 100644 compliance-dashboard/src/pages/issues.rs create mode 100644 compliance-dashboard/src/pages/mod.rs create mode 100644 compliance-dashboard/src/pages/overview.rs create mode 100644 compliance-dashboard/src/pages/repositories.rs create mode 100644 compliance-dashboard/src/pages/sbom.rs create mode 100644 compliance-dashboard/src/pages/settings.rs create mode 100644 docker-compose.yml create mode 100644 styles/input.css diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6902ae0 --- /dev/null +++ b/.env.example @@ -0,0 +1,39 @@ +# MongoDB +MONGODB_URI=mongodb://root:example@localhost:27017/compliance_scanner?authSource=admin +MONGODB_DATABASE=compliance_scanner + +# LiteLLM +LITELLM_URL=http://localhost:4000 +LITELLM_API_KEY= +LITELLM_MODEL=gpt-4o + +# GitHub +GITHUB_TOKEN= +GITHUB_WEBHOOK_SECRET= + +# GitLab +GITLAB_URL=https://gitlab.com +GITLAB_TOKEN= +GITLAB_WEBHOOK_SECRET= + +# Jira +JIRA_URL=https://your-org.atlassian.net +JIRA_EMAIL= +JIRA_API_TOKEN= +JIRA_PROJECT_KEY= + +# SearXNG +SEARXNG_URL=http://localhost:8888 + +# NVD +NVD_API_KEY= + +# Agent +AGENT_PORT=3001 +SCAN_SCHEDULE=0 0 */6 * * * +CVE_MONITOR_SCHEDULE=0 0 0 * * * +GIT_CLONE_BASE_PATH=/tmp/compliance-scanner/repos + +# Dashboard +DASHBOARD_PORT=8080 +AGENT_API_URL=http://localhost:3001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b92202 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +.env +*.swp +*.swo +*~ +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..618502d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5308 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "askama_escape" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df27b8d5ddb458c5fb1bbc1ce172d4a38c614a97d550b0ac89003897fb01de4" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-tungstenite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" +dependencies = [ + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tungstenite 0.27.0", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "axum-macros", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite 0.28.0", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bson" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" +dependencies = [ + "ahash", + "base64", + "bitvec", + "getrandom 0.2.17", + "getrandom 0.3.4", + "hex", + "indexmap", + "js-sys", + "once_cell", + "rand", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "charset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" +dependencies = [ + "base64", + "encoding_rs", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compliance-agent" +version = "0.1.0" +dependencies = [ + "axum", + "base64", + "chrono", + "compliance-core", + "dotenvy", + "futures-util", + "git2", + "hex", + "hmac", + "mongodb", + "octocrab", + "regex", + "reqwest", + "secrecy", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tokio", + "tokio-cron-scheduler", + "tower-http", + "tracing", + "tracing-subscriber", + "urlencoding", + "uuid", + "walkdir", +] + +[[package]] +name = "compliance-core" +version = "0.1.0" +dependencies = [ + "chrono", + "hex", + "mongodb", + "secrecy", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "uuid", +] + +[[package]] +name = "compliance-dashboard" +version = "0.1.0" +dependencies = [ + "axum", + "chrono", + "compliance-core", + "dioxus", + "dioxus-cli-config 0.7.3", + "dioxus-free-icons", + "dioxus-fullstack", + "dioxus-logger 0.6.2", + "dotenvy", + "mongodb", + "reqwest", + "secrecy", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tower-http", + "tracing", + "web-sys", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "const-serialize" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad7154afa56de2f290e3c82c2c6dc4f5b282b6870903f56ef3509aba95866edc" +dependencies = [ + "const-serialize-macro 0.7.2", +] + +[[package]] +name = "const-serialize" +version = "0.8.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e42cd5aabba86f128b3763da1fec1491c0f728ce99245062cd49b6f9e6d235b" +dependencies = [ + "const-serialize 0.7.2", + "const-serialize-macro 0.8.0-alpha.0", + "serde", +] + +[[package]] +name = "const-serialize-macro" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f160aad86b4343e8d4e261fee9965c3005b2fd6bc117d172ab65948779e4acf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "const-serialize-macro" +version = "0.8.0-alpha.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42571ed01eb46d2e1adcf99c8ca576f081e46f2623d13500eba70d1d99a4c439" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "const-str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "content_disposition" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc14a88e1463ddd193906285abe5c360c7e8564e05ccc5d501755f7fbc9ca9c" +dependencies = [ + "charset", +] + +[[package]] +name = "convert_case" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "croner" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c344b0690c1ad1c7176fe18eb173e0c927008fdaaa256e40dfd43ddd149c0843" +dependencies = [ + "chrono", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive-syn-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dioxus" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92b583b48ac77158495e6678fe3a2b5954fc8866fc04cb9695dd146e88bc329d" +dependencies = [ + "dioxus-asset-resolver", + "dioxus-cli-config 0.7.3", + "dioxus-config-macro", + "dioxus-config-macros", + "dioxus-core", + "dioxus-core-macro", + "dioxus-devtools", + "dioxus-document", + "dioxus-fullstack", + "dioxus-fullstack-macro", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-liveview", + "dioxus-logger 0.7.3", + "dioxus-router", + "dioxus-server", + "dioxus-signals", + "dioxus-ssr", + "dioxus-stores", + "dioxus-web", + "manganis", + "serde", + "subsecond", + "warnings", +] + +[[package]] +name = "dioxus-asset-resolver" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0161af1d3cfc8ff31503ff1b7ee0068c97771fc38d0cc6566e23483142ddf4f" +dependencies = [ + "dioxus-cli-config 0.7.3", + "http", + "infer", + "jni", + "js-sys", + "ndk", + "ndk-context", + "ndk-sys", + "percent-encoding", + "thiserror 2.0.18", + "tokio", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus-cli-config" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd16948f1ffdb068dd9a64812158073a4250e2af4e98ea31fdac0312e6bce86" + +[[package]] +name = "dioxus-cli-config" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd67ab405e1915a47df9769cd5408545d1b559d5c01ce7a0f442caef520d1f3" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "dioxus-config-macro" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f040ec7c41aa5428283f56bb0670afba9631bfe3ffd885f4814807f12c8c9d91" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "dioxus-config-macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c41b47b55a433b61f7c12327c85ba650572bacbcc42c342ba2e87a57975264" + +[[package]] +name = "dioxus-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b389b0e3cc01c7da292ad9b884b088835fdd1671d45fbd2f737506152b22eef0" +dependencies = [ + "anyhow", + "const_format", + "dioxus-core-types", + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash 2.1.1", + "rustversion", + "serde", + "slab", + "slotmap", + "subsecond", + "tracing", +] + +[[package]] +name = "dioxus-core-macro" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82d65f0024fc86f01911a16156d280eea583be5a82a3bed85e7e8e4194302d" +dependencies = [ + "convert_case 0.8.0", + "dioxus-rsx", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dioxus-core-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc4b8cdc440a55c17355542fc2089d97949bba674255d84cac77805e1db8c9f" + +[[package]] +name = "dioxus-devtools" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf89488bad8fb0f18b9086ee2db01f95f709801c10c68be42691a36378a0f2d" +dependencies = [ + "dioxus-cli-config 0.7.3", + "dioxus-core", + "dioxus-devtools-types", + "dioxus-signals", + "futures-channel", + "futures-util", + "serde", + "serde_json", + "subsecond", + "thiserror 2.0.18", + "tracing", + "tungstenite 0.27.0", +] + +[[package]] +name = "dioxus-devtools-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7381d9d7d0a0f66b9d5082d584853c3d53be21d34007073daca98ddf26fc4d" +dependencies = [ + "dioxus-core", + "serde", + "subsecond-types", +] + +[[package]] +name = "dioxus-document" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0aeeff26d9d06441f59fd8d7f4f76098ba30ca9728e047c94486161185ceb" +dependencies = [ + "dioxus-core", + "dioxus-core-macro", + "dioxus-core-types", + "dioxus-html", + "futures-channel", + "futures-util", + "generational-box", + "lazy-js-bundle", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "dioxus-free-icons" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d356e0f9edad0930bc1cc76744360c0ecca020cb943acaadf42cb774f28284" +dependencies = [ + "dioxus", +] + +[[package]] +name = "dioxus-fullstack" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db1f8b70338072ec408b48d09c96559cf071f87847465d8161294197504c498" +dependencies = [ + "anyhow", + "async-stream", + "async-tungstenite", + "axum", + "axum-core", + "axum-extra", + "base64", + "bytes", + "ciborium", + "const-str", + "const_format", + "content_disposition", + "derive_more", + "dioxus-asset-resolver", + "dioxus-cli-config 0.7.3", + "dioxus-core", + "dioxus-fullstack-core", + "dioxus-fullstack-macro", + "dioxus-hooks", + "dioxus-html", + "dioxus-signals", + "form_urlencoded", + "futures", + "futures-channel", + "futures-util", + "gloo-net", + "headers", + "http", + "http-body", + "http-body-util", + "inventory", + "js-sys", + "mime", + "pin-project", + "reqwest", + "rustversion", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.27.0", + "tokio-util", + "tower", + "tower-http", + "tower-layer", + "tracing", + "tungstenite 0.27.0", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "dioxus-fullstack-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda8b152e85121243741b9d5f2a3d8cb3c47a7b2299e902f98b6a7719915b0a2" +dependencies = [ + "anyhow", + "axum-core", + "base64", + "ciborium", + "dioxus-core", + "dioxus-document", + "dioxus-history", + "dioxus-hooks", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "http", + "inventory", + "parking_lot", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "dioxus-fullstack-macro" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "255104d4a4f278f1a8482fa30536c91d22260c561c954b753e72987df8d65b2e" +dependencies = [ + "const_format", + "convert_case 0.8.0", + "proc-macro2", + "quote", + "syn", + "xxhash-rust", +] + +[[package]] +name = "dioxus-history" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00ba43bfe6e5ca226fef6128f240ca970bea73cac0462416188026360ccdcf" +dependencies = [ + "dioxus-core", + "tracing", +] + +[[package]] +name = "dioxus-hooks" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab2da4f038c33cb38caa37ffc3f5d6dfbc018f05da35b238210a533bb075823" +dependencies = [ + "dioxus-core", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "rustversion", + "slab", + "tracing", +] + +[[package]] +name = "dioxus-html" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded5fa6d2e677b7442a93f4228bf3c0ad2597a8bd3292cae50c869d015f3a99" +dependencies = [ + "async-trait", + "bytes", + "dioxus-core", + "dioxus-core-macro", + "dioxus-core-types", + "dioxus-hooks", + "dioxus-html-internal-macro", + "enumset", + "euclid", + "futures-channel", + "futures-util", + "generational-box", + "keyboard-types", + "lazy-js-bundle", + "rustversion", + "serde", + "serde_json", + "serde_repr", + "tracing", +] + +[[package]] +name = "dioxus-html-internal-macro" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45462ab85fe059a36841508d40545109fd0e25855012d22583a61908eb5cd02a" +dependencies = [ + "convert_case 0.8.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dioxus-interpreter-js" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42a7f73ad32a5054bd8c1014f4ac78cca3b7f6889210ee2b57ea31b33b6d32f" +dependencies = [ + "dioxus-core", + "dioxus-core-types", + "dioxus-html", + "js-sys", + "lazy-js-bundle", + "rustc-hash 2.1.1", + "sledgehammer_bindgen", + "sledgehammer_utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus-liveview" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f7a1cfe6f8e9f2e303607c8ae564d11932fd80714c8a8c97e3860d55538997" +dependencies = [ + "axum", + "dioxus-cli-config 0.7.3", + "dioxus-core", + "dioxus-devtools", + "dioxus-document", + "dioxus-history", + "dioxus-html", + "dioxus-interpreter-js", + "futures-channel", + "futures-util", + "generational-box", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "slab", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "dioxus-logger" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545961e752f6c8bf59c274951b3c8b18a106db6ad2f9e2035b29e1f2a3e899b1" +dependencies = [ + "console_error_panic_hook", + "dioxus-cli-config 0.6.3", + "tracing", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "dioxus-logger" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1eeab114cb009d9e6b85ea10639a18cfc54bb342f3b837770b004c4daeb89c2" +dependencies = [ + "dioxus-cli-config 0.7.3", + "tracing", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "dioxus-router" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d5b31f9e27231389bf5a117b7074d22d8c58358b484a2558e56fbab20e64ca4" +dependencies = [ + "dioxus-cli-config 0.7.3", + "dioxus-core", + "dioxus-core-macro", + "dioxus-fullstack-core", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-router-macro", + "dioxus-signals", + "percent-encoding", + "rustversion", + "tracing", + "url", +] + +[[package]] +name = "dioxus-router-macro" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "838b9b441a95da62b39cae4defd240b5ebb0ec9f2daea1126099e00a838dc86f" +dependencies = [ + "base16", + "digest", + "proc-macro2", + "quote", + "sha2", + "slab", + "syn", +] + +[[package]] +name = "dioxus-rsx" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53128858f0ccca9de54292a4d48409fda1df75fd5012c6243f664042f0225d68" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "dioxus-server" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adb2d4e0f0f3a157bda6af2d90f22bac40070e509a66e3ea58abf3b35f904c" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "base64", + "bytes", + "chrono", + "ciborium", + "dashmap", + "dioxus-cli-config 0.7.3", + "dioxus-core", + "dioxus-core-macro", + "dioxus-devtools", + "dioxus-document", + "dioxus-fullstack-core", + "dioxus-history", + "dioxus-hooks", + "dioxus-html", + "dioxus-interpreter-js", + "dioxus-logger 0.7.3", + "dioxus-router", + "dioxus-signals", + "dioxus-ssr", + "enumset", + "futures", + "futures-channel", + "futures-util", + "generational-box", + "http", + "http-body-util", + "hyper", + "hyper-util", + "inventory", + "lru", + "parking_lot", + "pin-project", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "serde_qs", + "subsecond", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite 0.27.0", + "tokio-util", + "tower", + "tower-http", + "tracing", + "tracing-futures", + "url", + "walkdir", +] + +[[package]] +name = "dioxus-signals" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f48020bc23bc9766e7cce986c0fd6de9af0b8cbfd432652ec6b1094439c1ec6" +dependencies = [ + "dioxus-core", + "futures-channel", + "futures-util", + "generational-box", + "parking_lot", + "rustc-hash 2.1.1", + "tracing", + "warnings", +] + +[[package]] +name = "dioxus-ssr" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cf9294a21fcd1098e02ad7a3ba61b99be8310ad3395fecf8210387c83f26b9" +dependencies = [ + "askama_escape", + "dioxus-core", + "dioxus-core-types", + "rustc-hash 2.1.1", +] + +[[package]] +name = "dioxus-stores" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77aaa9ac56d781bb506cf3c0d23bea96b768064b89fe50d3b4d4659cc6bd8058" +dependencies = [ + "dioxus-core", + "dioxus-signals", + "dioxus-stores-macro", + "generational-box", +] + +[[package]] +name = "dioxus-stores-macro" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1a728622e7b63db45774f75e71504335dd4e6115b235bbcff272980499493a" +dependencies = [ + "convert_case 0.8.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dioxus-web" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b33fe739fed4e8143dac222a9153593f8e2451662ce8fc4c9d167a9d6ec0923" +dependencies = [ + "dioxus-cli-config 0.7.3", + "dioxus-core", + "dioxus-core-types", + "dioxus-devtools", + "dioxus-document", + "dioxus-fullstack-core", + "dioxus-history", + "dioxus-html", + "dioxus-interpreter-js", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "gloo-timers", + "js-sys", + "lazy-js-bundle", + "rustc-hash 2.1.1", + "send_wrapper", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generational-box" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4ed190b9de8e734d47a70be59b1e7588b9e8e0d0036e332f4c014e8aed1bc5" +dependencies = [ + "parking_lot", + "tracing", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "git2" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe 0.1.6", + "openssl-sys", + "url", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.2", + "system-configuration", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inventory" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[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.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags", + "serde", +] + +[[package]] +name = "lazy-js-bundle" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b88b715ab1496c6e6b8f5e927be961c4235196121b6ae59bcb51077a21dd36" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libgit2-sys" +version = "0.18.3+1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macro_magic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" +dependencies = [ + "macro_magic_core", + "macro_magic_macros", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" +dependencies = [ + "const-random", + "derive-syn-parse", + "macro_magic_core_macros", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_core_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macro_magic_macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" +dependencies = [ + "macro_magic_core", + "quote", + "syn", +] + +[[package]] +name = "manganis" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cce7d688848bf9d034168513b9a2ffbfe5f61df2ff14ae15e6cfc866efdd344" +dependencies = [ + "const-serialize 0.7.2", + "const-serialize 0.8.0-alpha.0", + "manganis-core", + "manganis-macro", +] + +[[package]] +name = "manganis-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ce917b978268fe8a7db49e216343ec7c8f471f7e686feb70940d67293f19d4" +dependencies = [ + "const-serialize 0.7.2", + "const-serialize 0.8.0-alpha.0", + "dioxus-cli-config 0.7.3", + "dioxus-core-types", + "serde", + "winnow", +] + +[[package]] +name = "manganis-macro" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad513e990f7c0bca86aa68659a7a3dc4c705572ed4c22fd6af32ccf261334cc2" +dependencies = [ + "dunce", + "macro-string", + "manganis-core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "mongocrypt" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da0cd419a51a5fb44819e290fbdb0665a54f21dead8923446a799c7f4d26ad9" +dependencies = [ + "bson", + "mongocrypt-sys", + "once_cell", + "serde", +] + +[[package]] +name = "mongocrypt-sys" +version = "0.1.5+1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" + +[[package]] +name = "mongodb" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803dd859e8afa084c255a8effd8000ff86f7c8076a50cd6d8c99e8f3496f75c2" +dependencies = [ + "base64", + "bitflags", + "bson", + "derive-where", + "derive_more", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hickory-proto", + "hickory-resolver", + "hmac", + "macro_magic", + "md-5", + "mongocrypt", + "mongodb-internal-macros", + "pbkdf2", + "percent-encoding", + "rand", + "rustc_version_runtime", + "rustls", + "rustversion", + "serde", + "serde_bytes", + "serde_with", + "sha1", + "sha2", + "socket2 0.6.2", + "stringprep", + "strsim", + "take_mut", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-util", + "typed-builder", + "uuid", + "webpki-roots", +] + +[[package]] +name = "mongodb-internal-macros" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973ef3dd3dbc6f6e65bbdecfd9ec5e781b9e7493b0f369a7c62e35d8e5ae2c8" +dependencies = [ + "macro_magic", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "octocrab" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86996964f8b721067b6ed238aa0ccee56ecad6ee5e714468aa567992d05d2b91" +dependencies = [ + "arc-swap", + "async-trait", + "base64", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", + "web-time", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.6.2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" +dependencies = [ + "rustc_version", + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_qs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "serde_core", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "sledgehammer_bindgen" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e83e178d176459c92bc129cfd0958afac3ced925471b889b3a75546cfc4133" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb251b407f50028476a600541542b605bb864d35d9ee1de4f6cab45d88475e6d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdd4b83524961983cea3c55383b3910fd2f24fd13a188f5b091d2d504a61ae" +dependencies = [ + "rustc-hash 1.1.0", +] + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subsecond" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8438668e545834d795d04c4335aafc332ce046106521a29f0a5c6501de34187c" +dependencies = [ + "js-sys", + "libc", + "libloading", + "memfd", + "memmap2", + "serde", + "subsecond-types", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "subsecond-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e72f747606fc19fe81d6c59e491af93ed7dcbcb6aad9d1d18b05129914ec298" +dependencies = [ + "serde", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-cron-scheduler" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5597b569b4712cf78aa0c9ae29742461b7bda1e49c2a5fdad1d79bf022f8f0" +dependencies = [ + "chrono", + "croner", + "num-derive", + "num-traits", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.27.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.28.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "iri-string", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "typed-builder" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warnings" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f68998838dab65727c9b30465595c6f7c953313559371ca8bf31759b3680ad" +dependencies = [ + "pin-project", + "tracing", + "warnings-macro", +] + +[[package]] +name = "warnings-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59195a1db0e95b920366d949ba5e0d3fc0e70b67c09be15ce5abb790106b0571" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..10cb989 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +members = ["compliance-core", "compliance-agent", "compliance-dashboard"] +resolver = "2" + +[workspace.lints.clippy] +unwrap_used = "deny" +expect_used = "deny" + +[workspace.dependencies] +compliance-core = { path = "compliance-core" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +chrono = { version = "0.4", features = ["serde"] } +mongodb = { version = "3", features = ["rustls-tls", "compat-3-0-0"] } +reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } +thiserror = "2" +sha2 = "0.10" +hex = "0.4" +uuid = { version = "1", features = ["v4", "serde"] } +secrecy = { version = "0.10", features = ["serde"] } +regex = "1" diff --git a/Dioxus.toml b/Dioxus.toml new file mode 100644 index 0000000..dc7b586 --- /dev/null +++ b/Dioxus.toml @@ -0,0 +1,11 @@ +[application] +name = "compliance-dashboard" +default_platform = "web" +asset_dir = "assets" + +[web.app] +title = "Compliance Scanner Dashboard" + +[web.watcher] +reload_html = true +watch_path = ["compliance-dashboard/src", "assets"] diff --git a/Dockerfile.agent b/Dockerfile.agent new file mode 100644 index 0000000..d8f54b6 --- /dev/null +++ b/Dockerfile.agent @@ -0,0 +1,14 @@ +FROM rust:1.89-bookworm AS builder + +WORKDIR /app +COPY . . +RUN cargo build --release -p compliance-agent + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates libssl3 git && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/compliance-agent /usr/local/bin/compliance-agent + +EXPOSE 3001 3002 + +ENTRYPOINT ["compliance-agent"] diff --git a/Dockerfile.dashboard b/Dockerfile.dashboard new file mode 100644 index 0000000..252d952 --- /dev/null +++ b/Dockerfile.dashboard @@ -0,0 +1,18 @@ +FROM rust:1.89-bookworm AS builder + +RUN cargo install dioxus-cli --version 0.7.3 + +WORKDIR /app +COPY . . +RUN dx build --release --features server --platform web + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/compliance-dashboard /usr/local/bin/compliance-dashboard +COPY --from=builder /app/target/dx/compliance-dashboard/release/web/public /app/public + +EXPOSE 8080 + +WORKDIR /app +ENTRYPOINT ["compliance-dashboard"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..698d9f9 --- /dev/null +++ b/README.md @@ -0,0 +1,205 @@ +

+ Compliance Scanner Logo +

+ +

Compliance Scanner

+ +

+ Autonomous security and compliance scanning agent for git repositories +

+ +

+ Rust + Dioxus + MongoDB + Axum + Tailwind CSS +

+ +

+ GDPR + OAuth + SAST + CVE + Platform +

+ +--- + +## About + +Compliance Scanner is an autonomous agent that continuously monitors git repositories for security vulnerabilities, GDPR/OAuth compliance patterns, and dependency risks. It creates issues in external trackers (GitHub/GitLab/Jira) with evidence and remediation suggestions, reviews pull requests, and exposes a Dioxus-based dashboard for visualization. + +> **How it works:** The agent runs as a lazy daemon -- it only scans when new commits are detected, triggered by cron schedules or webhooks. LLM-powered triage filters out false positives and generates actionable remediation. + +## Features + +| Area | Capabilities | +|------|-------------| +| **SAST Scanning** | Semgrep-based static analysis with auto-config rules | +| **SBOM Generation** | Syft + cargo-audit for complete dependency inventory | +| **CVE Monitoring** | OSV.dev batch queries, NVD CVSS enrichment, SearXNG context | +| **GDPR Patterns** | Detect PII logging, missing consent, hardcoded retention, missing deletion | +| **OAuth Patterns** | Detect implicit grant, missing PKCE, token in localStorage, token in URLs | +| **LLM Triage** | Confidence scoring via LiteLLM to filter false positives | +| **Issue Creation** | Auto-create issues in GitHub, GitLab, or Jira with code evidence | +| **PR Reviews** | Post security review comments on pull requests | +| **Dashboard** | Fullstack Dioxus UI with findings, SBOM, issues, and statistics | +| **Webhooks** | GitHub (HMAC-SHA256) and GitLab webhook receivers for push/PR events | + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Cargo Workspace │ +├──────────────┬──────────────────┬───────────────────────────┤ +│ compliance- │ compliance- │ compliance- │ +│ core │ agent │ dashboard │ +│ (lib) │ (bin) │ (bin, Dioxus 0.7.3) │ +│ │ │ │ +│ Models │ Scan Pipeline │ Fullstack Web UI │ +│ Traits │ LLM Client │ Server Functions │ +│ Config │ Issue Trackers │ Charts + Tables │ +│ Errors │ Scheduler │ Settings Page │ +│ │ REST API │ │ +│ │ Webhooks │ │ +└──────────────┴──────────────────┴───────────────────────────┘ + │ + MongoDB (shared) +``` + +## Scan Pipeline (7 Stages) + +1. **Change Detection** -- `git2` fetch, compare HEAD SHA with last scanned commit +2. **Semgrep SAST** -- CLI wrapper with JSON output parsing +3. **SBOM Generation** -- Syft (CycloneDX) + cargo-audit vulnerability merge +4. **CVE Scanning** -- OSV.dev batch + NVD CVSS enrichment + SearXNG context +5. **Pattern Scanning** -- Regex-based GDPR and OAuth compliance checks +6. **LLM Triage** -- LiteLLM confidence scoring, filter findings < 3/10 +7. **Issue Creation** -- Dedup via SHA-256 fingerprint, create tracker issues + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Shared Library | `compliance-core` -- models, traits, config | +| Agent | Axum REST API, git2, tokio-cron-scheduler, Semgrep, Syft | +| Dashboard | Dioxus 0.7.3 fullstack, Tailwind CSS | +| Database | MongoDB with typed collections | +| LLM | LiteLLM (OpenAI-compatible API) | +| Issue Trackers | GitHub (octocrab), GitLab (REST v4), Jira (REST v3) | +| CVE Sources | OSV.dev, NVD, SearXNG | + +## Getting Started + +### Prerequisites + +- Rust 1.89+ +- [Dioxus CLI](https://dioxuslabs.com/learn/0.7/getting_started) (`dx`) +- MongoDB +- Docker & Docker Compose (optional) + +### Optional External Tools + +- [Semgrep](https://semgrep.dev/) -- for SAST scanning +- [Syft](https://github.com/anchore/syft) -- for SBOM generation +- [cargo-audit](https://github.com/rustsec/rustsec) -- for Rust dependency auditing + +### Setup + +```bash +# Clone the repository +git clone +cd compliance-scanner + +# Start MongoDB + SearXNG +docker compose up -d mongo searxng + +# Configure environment +cp .env.example .env +# Edit .env with your LiteLLM, tracker tokens, and MongoDB settings + +# Run the agent +cargo run -p compliance-agent + +# Run the dashboard (separate terminal) +dx serve --features server --platform web +``` + +### Docker Compose (Full Stack) + +```bash +docker compose up -d +``` + +This starts MongoDB, SearXNG, the agent (port 3001), and the dashboard (port 8080). + +## REST API + +The agent exposes a REST API on port 3001: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api/v1/health` | Health check | +| `GET` | `/api/v1/stats/overview` | Summary statistics and trends | +| `GET` | `/api/v1/repositories` | List tracked repositories | +| `POST` | `/api/v1/repositories` | Add a repository to track | +| `POST` | `/api/v1/repositories/:id/scan` | Trigger a manual scan | +| `GET` | `/api/v1/findings` | List findings (filterable) | +| `GET` | `/api/v1/findings/:id` | Get finding with code evidence | +| `PATCH` | `/api/v1/findings/:id/status` | Update finding status | +| `GET` | `/api/v1/sbom` | List dependencies | +| `GET` | `/api/v1/issues` | List cross-tracker issues | +| `GET` | `/api/v1/scan-runs` | Scan execution history | +| `POST` | `/webhook/github` | GitHub webhook (HMAC-SHA256) | +| `POST` | `/webhook/gitlab` | GitLab webhook (token verify) | + +## Dashboard Pages + +| Page | Description | +|------|-------------| +| **Overview** | Stat cards, severity distribution chart | +| **Repositories** | Add/manage tracked repos, trigger scans | +| **Findings** | Filterable table by severity, type, status | +| **Finding Detail** | Code evidence, remediation, suggested fix, linked issue | +| **SBOM** | Dependency inventory with vulnerability badges | +| **Issues** | Cross-tracker view (GitHub + GitLab + Jira) | +| **Settings** | Configure LiteLLM, tracker tokens, SearXNG URL | + +## Project Structure + +``` +compliance-scanner/ +├── compliance-core/ Shared library (models, traits, config, errors) +├── compliance-agent/ Agent daemon (pipeline, LLM, trackers, API, webhooks) +│ └── src/ +│ ├── pipeline/ 7-stage scan pipeline +│ ├── llm/ LiteLLM client, triage, descriptions, fixes, PR review +│ ├── trackers/ GitHub, GitLab, Jira integrations +│ ├── api/ REST API (Axum) +│ └── webhooks/ GitHub + GitLab webhook receivers +├── compliance-dashboard/ Dioxus fullstack dashboard +│ └── src/ +│ ├── components/ Reusable UI components +│ ├── infrastructure/ Server functions, DB, config +│ └── pages/ Full page views +├── assets/ Static assets (CSS, icons) +├── styles/ Tailwind input stylesheet +└── bin/ Dashboard binary entrypoint +``` + +## External Services + +| Service | Purpose | Default URL | +|---------|---------|-------------| +| MongoDB | Persistence | `mongodb://localhost:27017` | +| LiteLLM | LLM proxy for triage and generation | `http://localhost:4000` | +| SearXNG | CVE context search | `http://localhost:8888` | +| Semgrep | SAST scanning | CLI tool | +| Syft | SBOM generation | CLI tool | + +--- + +

+ Built with Rust, Dioxus, and a commitment to automated security compliance. +

diff --git a/assets/favicon.svg b/assets/favicon.svg new file mode 100644 index 0000000..0ba70b3 --- /dev/null +++ b/assets/favicon.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/main.css b/assets/main.css new file mode 100644 index 0000000..4bbdd35 --- /dev/null +++ b/assets/main.css @@ -0,0 +1,315 @@ +:root { + --sidebar-width: 260px; + --header-height: 56px; + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-card: #1e293b; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --accent: #38bdf8; + --accent-hover: #7dd3fc; + --border: #334155; + --danger: #ef4444; + --warning: #f59e0b; + --success: #22c55e; + --info: #3b82f6; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; +} + +.app-shell { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: var(--sidebar-width); + background: var(--bg-secondary); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 40; + overflow-y: auto; +} + +.sidebar-header { + padding: 20px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 12px; +} + +.sidebar-header h1 { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); +} + +.sidebar-nav { + padding: 12px 8px; + flex: 1; +} + +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + color: var(--text-secondary); + text-decoration: none; + font-size: 14px; + font-weight: 500; + transition: all 0.15s; + cursor: pointer; +} + +.nav-item:hover { + background: rgba(56, 189, 248, 0.1); + color: var(--text-primary); +} + +.nav-item.active { + background: rgba(56, 189, 248, 0.15); + color: var(--accent); +} + +.main-content { + margin-left: var(--sidebar-width); + flex: 1; + padding: 24px 32px; + min-height: 100vh; +} + +.page-header { + margin-bottom: 24px; +} + +.page-header h2 { + font-size: 24px; + font-weight: 700; +} + +.page-header p { + color: var(--text-secondary); + margin-top: 4px; +} + +.stat-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; +} + +.stat-card .label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.stat-card .value { + font-size: 28px; + font-weight: 700; +} + +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; + margin-bottom: 16px; +} + +.card-header { + font-size: 16px; + font-weight: 600; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} + +.table-wrapper { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + text-align: left; + padding: 12px 16px; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + border-bottom: 1px solid var(--border); + font-weight: 600; +} + +td { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + font-size: 14px; +} + +tr:hover { + background: rgba(56, 189, 248, 0.05); +} + +.badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 9999px; + font-size: 12px; + font-weight: 600; +} + +.badge-critical { background: rgba(239, 68, 68, 0.2); color: #fca5a5; } +.badge-high { background: rgba(249, 115, 22, 0.2); color: #fdba74; } +.badge-medium { background: rgba(245, 158, 11, 0.2); color: #fcd34d; } +.badge-low { background: rgba(34, 197, 94, 0.2); color: #86efac; } +.badge-info { background: rgba(59, 130, 246, 0.2); color: #93c5fd; } + +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.15s; +} + +.btn-primary { + background: var(--accent); + color: #0f172a; +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-ghost { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border); +} + +.btn-ghost:hover { + color: var(--text-primary); + border-color: var(--text-secondary); +} + +.code-block { + background: #0d1117; + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 13px; + line-height: 1.6; + overflow-x: auto; + white-space: pre; +} + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 16px; +} + +.filter-bar { + display: flex; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.filter-bar select, +.filter-bar input { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px 12px; + color: var(--text-primary); + font-size: 14px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + font-size: 14px; + font-weight: 500; + margin-bottom: 6px; + color: var(--text-secondary); +} + +.form-group input, +.form-group select { + width: 100%; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 14px; + color: var(--text-primary); + font-size: 14px; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: var(--text-secondary); +} + +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + transition: transform 0.3s; + } + .sidebar.open { + transform: translateX(0); + } + .main-content { + margin-left: 0; + padding: 16px; + } +} diff --git a/assets/tailwind.css b/assets/tailwind.css new file mode 100644 index 0000000..314f56a --- /dev/null +++ b/assets/tailwind.css @@ -0,0 +1 @@ +/* Placeholder - generated by build.rs via bunx @tailwindcss/cli */ diff --git a/bin/main.rs b/bin/main.rs new file mode 100644 index 0000000..65fdbcd --- /dev/null +++ b/bin/main.rs @@ -0,0 +1,23 @@ +#![allow(non_snake_case)] + +#[allow(clippy::expect_used)] +fn main() { + dioxus_logger::init(tracing::Level::DEBUG).expect("Failed to init logger"); + + #[cfg(feature = "web")] + { + dioxus::web::launch::launch_cfg( + compliance_dashboard::App, + dioxus::web::Config::new().hydrate(true), + ); + } + + #[cfg(feature = "server")] + { + compliance_dashboard::infrastructure::server_start(compliance_dashboard::App) + .map_err(|e| { + tracing::error!("Unable to start server: {e}"); + }) + .expect("Server start failed") + } +} diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..0bbd2fc --- /dev/null +++ b/build.rs @@ -0,0 +1,24 @@ +fn main() -> Result<(), Box> { + use std::process::Command; + println!("cargo:rerun-if-changed=./styles/input.css"); + + match Command::new("bunx") + .args([ + "@tailwindcss/cli", + "-i", + "./styles/input.css", + "-o", + "./assets/tailwind.css", + ]) + .status() + { + Ok(status) if !status.success() => { + println!("cargo:warning=tailwind build exited with {status}, skipping CSS generation"); + } + Err(e) => { + println!("cargo:warning=bunx not found ({e}), skipping tailwind CSS generation"); + } + Ok(_) => {} + } + Ok(()) +} diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..cda8d17 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +avoid-breaking-exported-api = false diff --git a/compliance-agent/Cargo.toml b/compliance-agent/Cargo.toml new file mode 100644 index 0000000..aabd3ea --- /dev/null +++ b/compliance-agent/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "compliance-agent" +version = "0.1.0" +edition = "2021" + +[lints] +workspace = true + +[dependencies] +compliance-core = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +chrono = { workspace = true } +mongodb = { workspace = true } +reqwest = { workspace = true } +thiserror = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +uuid = { workspace = true } +secrecy = { workspace = true } +regex = { workspace = true } +axum = "0.8" +tower-http = { version = "0.6", features = ["cors", "trace"] } +git2 = "0.20" +octocrab = "0.44" +tokio-cron-scheduler = "0.13" +dotenvy = "0.15" +hmac = "0.12" +walkdir = "2" +base64 = "0.22" +urlencoding = "2" +futures-util = "0.3" diff --git a/compliance-agent/src/agent.rs b/compliance-agent/src/agent.rs new file mode 100644 index 0000000..1577acb --- /dev/null +++ b/compliance-agent/src/agent.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; + +use compliance_core::AgentConfig; + +use crate::database::Database; +use crate::llm::LlmClient; +use crate::pipeline::orchestrator::PipelineOrchestrator; + +#[derive(Clone)] +pub struct ComplianceAgent { + pub config: AgentConfig, + pub db: Database, + pub llm: Arc, + pub http: reqwest::Client, +} + +impl ComplianceAgent { + pub fn new(config: AgentConfig, db: Database) -> Self { + let llm = Arc::new(LlmClient::new( + config.litellm_url.clone(), + config.litellm_api_key.clone(), + config.litellm_model.clone(), + )); + Self { + config, + db, + llm, + http: reqwest::Client::new(), + } + } + + pub async fn run_scan( + &self, + repo_id: &str, + trigger: compliance_core::models::ScanTrigger, + ) -> Result<(), crate::error::AgentError> { + let orchestrator = PipelineOrchestrator::new( + self.config.clone(), + self.db.clone(), + self.llm.clone(), + self.http.clone(), + ); + orchestrator.run(repo_id, trigger).await + } +} diff --git a/compliance-agent/src/api/handlers/mod.rs b/compliance-agent/src/api/handlers/mod.rs new file mode 100644 index 0000000..43e39fc --- /dev/null +++ b/compliance-agent/src/api/handlers/mod.rs @@ -0,0 +1,334 @@ +use std::sync::Arc; + +#[allow(unused_imports)] +use axum::extract::{Extension, Path, Query}; +use axum::http::StatusCode; +use axum::Json; +use mongodb::bson::doc; +use serde::{Deserialize, Serialize}; + +use compliance_core::models::*; + +use crate::agent::ComplianceAgent; + +#[derive(Deserialize)] +pub struct PaginationParams { + #[serde(default = "default_page")] + pub page: u64, + #[serde(default = "default_limit")] + pub limit: i64, +} + +fn default_page() -> u64 { 1 } +fn default_limit() -> i64 { 50 } + +#[derive(Deserialize)] +pub struct FindingsFilter { + #[serde(default)] + pub repo_id: Option, + #[serde(default)] + pub severity: Option, + #[serde(default)] + pub scan_type: Option, + #[serde(default)] + pub status: Option, + #[serde(default = "default_page")] + pub page: u64, + #[serde(default = "default_limit")] + pub limit: i64, +} + +#[derive(Serialize)] +pub struct ApiResponse { + pub data: T, + #[serde(skip_serializing_if = "Option::is_none")] + pub total: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, +} + +#[derive(Serialize)] +pub struct OverviewStats { + pub total_repositories: u64, + pub total_findings: u64, + pub critical_findings: u64, + pub high_findings: u64, + pub medium_findings: u64, + pub low_findings: u64, + pub total_sbom_entries: u64, + pub total_cve_alerts: u64, + pub total_issues: u64, + pub recent_scans: Vec, +} + +#[derive(Deserialize)] +pub struct AddRepositoryRequest { + pub name: String, + pub git_url: String, + #[serde(default = "default_branch")] + pub default_branch: String, + pub tracker_type: Option, + pub tracker_owner: Option, + pub tracker_repo: Option, + pub scan_schedule: Option, +} + +fn default_branch() -> String { "main".to_string() } + +#[derive(Deserialize)] +pub struct UpdateStatusRequest { + pub status: String, +} + +type AgentExt = Extension>; +type ApiResult = Result>, StatusCode>; + +pub async fn health() -> Json { + Json(serde_json::json!({ "status": "ok" })) +} + +pub async fn stats_overview(Extension(agent): AgentExt) -> ApiResult { + let db = &agent.db; + + let total_repositories = db.repositories().count_documents(doc! {}).await.unwrap_or(0); + let total_findings = db.findings().count_documents(doc! {}).await.unwrap_or(0); + let critical_findings = db.findings().count_documents(doc! { "severity": "critical" }).await.unwrap_or(0); + let high_findings = db.findings().count_documents(doc! { "severity": "high" }).await.unwrap_or(0); + let medium_findings = db.findings().count_documents(doc! { "severity": "medium" }).await.unwrap_or(0); + let low_findings = db.findings().count_documents(doc! { "severity": "low" }).await.unwrap_or(0); + let total_sbom_entries = db.sbom_entries().count_documents(doc! {}).await.unwrap_or(0); + let total_cve_alerts = db.cve_alerts().count_documents(doc! {}).await.unwrap_or(0); + let total_issues = db.tracker_issues().count_documents(doc! {}).await.unwrap_or(0); + + let recent_scans: Vec = match db + .scan_runs() + .find(doc! {}) + .sort(doc! { "started_at": -1 }) + .limit(10) + .await + { + Ok(cursor) => collect_cursor_async(cursor).await, + Err(_) => Vec::new(), + }; + + Ok(Json(ApiResponse { + data: OverviewStats { + total_repositories, + total_findings, + critical_findings, + high_findings, + medium_findings, + low_findings, + total_sbom_entries, + total_cve_alerts, + total_issues, + recent_scans, + }, + total: None, + page: None, + })) +} + +pub async fn list_repositories( + Extension(agent): AgentExt, + Query(params): Query, +) -> ApiResult> { + let db = &agent.db; + let skip = (params.page.saturating_sub(1)) * params.limit as u64; + let total = db.repositories().count_documents(doc! {}).await.unwrap_or(0); + + let repos = match db.repositories().find(doc! {}).skip(skip).limit(params.limit).await { + Ok(cursor) => collect_cursor_async(cursor).await, + Err(_) => Vec::new(), + }; + + Ok(Json(ApiResponse { + data: repos, + total: Some(total), + page: Some(params.page), + })) +} + +pub async fn add_repository( + Extension(agent): AgentExt, + Json(req): Json, +) -> Result>, StatusCode> { + let mut repo = TrackedRepository::new(req.name, req.git_url); + repo.default_branch = req.default_branch; + repo.tracker_type = req.tracker_type; + repo.tracker_owner = req.tracker_owner; + repo.tracker_repo = req.tracker_repo; + repo.scan_schedule = req.scan_schedule; + + agent + .db + .repositories() + .insert_one(&repo) + .await + .map_err(|_| StatusCode::CONFLICT)?; + + Ok(Json(ApiResponse { + data: repo, + total: None, + page: None, + })) +} + +pub async fn trigger_scan( + Extension(agent): AgentExt, + Path(id): Path, +) -> Result, StatusCode> { + let agent_clone = (*agent).clone(); + tokio::spawn(async move { + if let Err(e) = agent_clone.run_scan(&id, ScanTrigger::Manual).await { + tracing::error!("Manual scan failed for {id}: {e}"); + } + }); + + Ok(Json(serde_json::json!({ "status": "scan_triggered" }))) +} + +pub async fn list_findings( + Extension(agent): AgentExt, + Query(filter): Query, +) -> ApiResult> { + let db = &agent.db; + let mut query = doc! {}; + if let Some(repo_id) = &filter.repo_id { + query.insert("repo_id", repo_id); + } + if let Some(severity) = &filter.severity { + query.insert("severity", severity); + } + if let Some(scan_type) = &filter.scan_type { + query.insert("scan_type", scan_type); + } + if let Some(status) = &filter.status { + query.insert("status", status); + } + + let skip = (filter.page.saturating_sub(1)) * filter.limit as u64; + let total = db.findings().count_documents(query.clone()).await.unwrap_or(0); + + let findings = match db.findings().find(query).sort(doc! { "created_at": -1 }).skip(skip).limit(filter.limit).await { + Ok(cursor) => collect_cursor_async(cursor).await, + Err(_) => Vec::new(), + }; + + Ok(Json(ApiResponse { + data: findings, + total: Some(total), + page: Some(filter.page), + })) +} + +pub async fn get_finding( + Extension(agent): AgentExt, + Path(id): Path, +) -> Result>, StatusCode> { + let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?; + let finding = agent + .db + .findings() + .find_one(doc! { "_id": oid }) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::NOT_FOUND)?; + + Ok(Json(ApiResponse { + data: finding, + total: None, + page: None, + })) +} + +pub async fn update_finding_status( + Extension(agent): AgentExt, + Path(id): Path, + Json(req): Json, +) -> Result, StatusCode> { + let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?; + + agent + .db + .findings() + .update_one( + doc! { "_id": oid }, + doc! { "$set": { "status": &req.status, "updated_at": mongodb::bson::DateTime::now() } }, + ) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(serde_json::json!({ "status": "updated" }))) +} + +pub async fn list_sbom( + Extension(agent): AgentExt, + Query(params): Query, +) -> ApiResult> { + let db = &agent.db; + let skip = (params.page.saturating_sub(1)) * params.limit as u64; + let total = db.sbom_entries().count_documents(doc! {}).await.unwrap_or(0); + + let entries = match db.sbom_entries().find(doc! {}).skip(skip).limit(params.limit).await { + Ok(cursor) => collect_cursor_async(cursor).await, + Err(_) => Vec::new(), + }; + + Ok(Json(ApiResponse { + data: entries, + total: Some(total), + page: Some(params.page), + })) +} + +pub async fn list_issues( + Extension(agent): AgentExt, + Query(params): Query, +) -> ApiResult> { + let db = &agent.db; + let skip = (params.page.saturating_sub(1)) * params.limit as u64; + let total = db.tracker_issues().count_documents(doc! {}).await.unwrap_or(0); + + let issues = match db.tracker_issues().find(doc! {}).sort(doc! { "created_at": -1 }).skip(skip).limit(params.limit).await { + Ok(cursor) => collect_cursor_async(cursor).await, + Err(_) => Vec::new(), + }; + + Ok(Json(ApiResponse { + data: issues, + total: Some(total), + page: Some(params.page), + })) +} + +pub async fn list_scan_runs( + Extension(agent): AgentExt, + Query(params): Query, +) -> ApiResult> { + let db = &agent.db; + let skip = (params.page.saturating_sub(1)) * params.limit as u64; + let total = db.scan_runs().count_documents(doc! {}).await.unwrap_or(0); + + let scans = match db.scan_runs().find(doc! {}).sort(doc! { "started_at": -1 }).skip(skip).limit(params.limit).await { + Ok(cursor) => collect_cursor_async(cursor).await, + Err(_) => Vec::new(), + }; + + Ok(Json(ApiResponse { + data: scans, + total: Some(total), + page: Some(params.page), + })) +} + +async fn collect_cursor_async( + mut cursor: mongodb::Cursor, +) -> Vec { + use futures_util::StreamExt; + let mut items = Vec::new(); + while let Some(Ok(item)) = cursor.next().await { + items.push(item); + } + items +} diff --git a/compliance-agent/src/api/mod.rs b/compliance-agent/src/api/mod.rs new file mode 100644 index 0000000..43a142c --- /dev/null +++ b/compliance-agent/src/api/mod.rs @@ -0,0 +1,5 @@ +pub mod handlers; +pub mod routes; +pub mod server; + +pub use server::start_api_server; diff --git a/compliance-agent/src/api/routes.rs b/compliance-agent/src/api/routes.rs new file mode 100644 index 0000000..b7a1190 --- /dev/null +++ b/compliance-agent/src/api/routes.rs @@ -0,0 +1,19 @@ +use axum::routing::{get, patch, post}; +use axum::Router; + +use crate::api::handlers; + +pub fn build_router() -> Router { + Router::new() + .route("/api/v1/health", get(handlers::health)) + .route("/api/v1/stats/overview", get(handlers::stats_overview)) + .route("/api/v1/repositories", get(handlers::list_repositories)) + .route("/api/v1/repositories", post(handlers::add_repository)) + .route("/api/v1/repositories/{id}/scan", post(handlers::trigger_scan)) + .route("/api/v1/findings", get(handlers::list_findings)) + .route("/api/v1/findings/{id}", get(handlers::get_finding)) + .route("/api/v1/findings/{id}/status", patch(handlers::update_finding_status)) + .route("/api/v1/sbom", get(handlers::list_sbom)) + .route("/api/v1/issues", get(handlers::list_issues)) + .route("/api/v1/scan-runs", get(handlers::list_scan_runs)) +} diff --git a/compliance-agent/src/api/server.rs b/compliance-agent/src/api/server.rs new file mode 100644 index 0000000..f6c1979 --- /dev/null +++ b/compliance-agent/src/api/server.rs @@ -0,0 +1,28 @@ +use std::sync::Arc; + +use axum::Extension; +use tower_http::cors::CorsLayer; +use tower_http::trace::TraceLayer; + +use crate::agent::ComplianceAgent; +use crate::api::routes; +use crate::error::AgentError; + +pub async fn start_api_server(agent: ComplianceAgent, port: u16) -> Result<(), AgentError> { + let app = routes::build_router() + .layer(Extension(Arc::new(agent))) + .layer(CorsLayer::permissive()) + .layer(TraceLayer::new_for_http()); + + let addr = format!("0.0.0.0:{port}"); + let listener = tokio::net::TcpListener::bind(&addr) + .await + .map_err(|e| AgentError::Other(format!("Failed to bind to {addr}: {e}")))?; + + tracing::info!("REST API listening on {addr}"); + axum::serve(listener, app) + .await + .map_err(|e| AgentError::Other(format!("API server error: {e}")))?; + + Ok(()) +} diff --git a/compliance-agent/src/config.rs b/compliance-agent/src/config.rs new file mode 100644 index 0000000..92b27a6 --- /dev/null +++ b/compliance-agent/src/config.rs @@ -0,0 +1,43 @@ +use compliance_core::AgentConfig; +use secrecy::SecretString; + +use crate::error::AgentError; + +fn env_var(key: &str) -> Result { + std::env::var(key).map_err(|_| AgentError::Config(format!("Missing env var: {key}"))) +} + +fn env_var_opt(key: &str) -> Option { + std::env::var(key).ok().filter(|v| !v.is_empty()) +} + +fn env_secret_opt(key: &str) -> Option { + env_var_opt(key).map(SecretString::from) +} + +pub fn load_config() -> Result { + Ok(AgentConfig { + mongodb_uri: env_var("MONGODB_URI")?, + mongodb_database: env_var_opt("MONGODB_DATABASE").unwrap_or_else(|| "compliance_scanner".to_string()), + litellm_url: env_var_opt("LITELLM_URL").unwrap_or_else(|| "http://localhost:4000".to_string()), + litellm_api_key: SecretString::from(env_var_opt("LITELLM_API_KEY").unwrap_or_default()), + litellm_model: env_var_opt("LITELLM_MODEL").unwrap_or_else(|| "gpt-4o".to_string()), + github_token: env_secret_opt("GITHUB_TOKEN"), + github_webhook_secret: env_secret_opt("GITHUB_WEBHOOK_SECRET"), + gitlab_url: env_var_opt("GITLAB_URL"), + gitlab_token: env_secret_opt("GITLAB_TOKEN"), + gitlab_webhook_secret: env_secret_opt("GITLAB_WEBHOOK_SECRET"), + jira_url: env_var_opt("JIRA_URL"), + jira_email: env_var_opt("JIRA_EMAIL"), + jira_api_token: env_secret_opt("JIRA_API_TOKEN"), + jira_project_key: env_var_opt("JIRA_PROJECT_KEY"), + searxng_url: env_var_opt("SEARXNG_URL"), + nvd_api_key: env_secret_opt("NVD_API_KEY"), + agent_port: env_var_opt("AGENT_PORT") + .and_then(|p| p.parse().ok()) + .unwrap_or(3001), + scan_schedule: env_var_opt("SCAN_SCHEDULE").unwrap_or_else(|| "0 0 */6 * * *".to_string()), + cve_monitor_schedule: env_var_opt("CVE_MONITOR_SCHEDULE").unwrap_or_else(|| "0 0 0 * * *".to_string()), + git_clone_base_path: env_var_opt("GIT_CLONE_BASE_PATH").unwrap_or_else(|| "/tmp/compliance-scanner/repos".to_string()), + }) +} diff --git a/compliance-agent/src/database.rs b/compliance-agent/src/database.rs new file mode 100644 index 0000000..16b3408 --- /dev/null +++ b/compliance-agent/src/database.rs @@ -0,0 +1,122 @@ +use mongodb::bson::doc; +use mongodb::{Client, Collection, IndexModel}; +use mongodb::options::IndexOptions; + +use compliance_core::models::*; + +use crate::error::AgentError; + +#[derive(Clone, Debug)] +pub struct Database { + inner: mongodb::Database, +} + +impl Database { + pub async fn connect(uri: &str, db_name: &str) -> Result { + let client = Client::with_uri_str(uri).await?; + let db = client.database(db_name); + db.run_command(doc! { "ping": 1 }).await?; + tracing::info!("Connected to MongoDB database '{db_name}'"); + Ok(Self { inner: db }) + } + + pub async fn ensure_indexes(&self) -> Result<(), AgentError> { + // repositories: unique git_url + self.repositories() + .create_index( + IndexModel::builder() + .keys(doc! { "git_url": 1 }) + .options(IndexOptions::builder().unique(true).build()) + .build(), + ) + .await?; + + // findings: unique fingerprint + self.findings() + .create_index( + IndexModel::builder() + .keys(doc! { "fingerprint": 1 }) + .options(IndexOptions::builder().unique(true).build()) + .build(), + ) + .await?; + + // findings: repo_id + severity compound + self.findings() + .create_index( + IndexModel::builder() + .keys(doc! { "repo_id": 1, "severity": 1 }) + .build(), + ) + .await?; + + // scan_runs: repo_id + started_at descending + self.scan_runs() + .create_index( + IndexModel::builder() + .keys(doc! { "repo_id": 1, "started_at": -1 }) + .build(), + ) + .await?; + + // sbom_entries: compound + self.sbom_entries() + .create_index( + IndexModel::builder() + .keys(doc! { "repo_id": 1, "name": 1, "version": 1 }) + .build(), + ) + .await?; + + // cve_alerts: unique cve_id + repo_id + self.cve_alerts() + .create_index( + IndexModel::builder() + .keys(doc! { "cve_id": 1, "repo_id": 1 }) + .options(IndexOptions::builder().unique(true).build()) + .build(), + ) + .await?; + + // tracker_issues: unique finding_id + self.tracker_issues() + .create_index( + IndexModel::builder() + .keys(doc! { "finding_id": 1 }) + .options(IndexOptions::builder().unique(true).build()) + .build(), + ) + .await?; + + tracing::info!("Database indexes ensured"); + Ok(()) + } + + pub fn repositories(&self) -> Collection { + self.inner.collection("repositories") + } + + pub fn findings(&self) -> Collection { + self.inner.collection("findings") + } + + pub fn scan_runs(&self) -> Collection { + self.inner.collection("scan_runs") + } + + pub fn sbom_entries(&self) -> Collection { + self.inner.collection("sbom_entries") + } + + pub fn cve_alerts(&self) -> Collection { + self.inner.collection("cve_alerts") + } + + pub fn tracker_issues(&self) -> Collection { + self.inner.collection("tracker_issues") + } + + pub fn raw_collection(&self, name: &str) -> Collection { + self.inner.collection(name) + } +} diff --git a/compliance-agent/src/error.rs b/compliance-agent/src/error.rs new file mode 100644 index 0000000..cee4751 --- /dev/null +++ b/compliance-agent/src/error.rs @@ -0,0 +1,41 @@ +use compliance_core::CoreError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AgentError { + #[error(transparent)] + Core(#[from] CoreError), + + #[error("Database error: {0}")] + Database(#[from] mongodb::error::Error), + + #[error("Git error: {0}")] + Git(#[from] git2::Error), + + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Scheduler error: {0}")] + Scheduler(String), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("{0}")] + Other(String), +} + +impl From for axum::http::StatusCode { + fn from(err: AgentError) -> Self { + match err { + AgentError::Core(CoreError::NotFound(_)) => axum::http::StatusCode::NOT_FOUND, + _ => axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} diff --git a/compliance-agent/src/llm/client.rs b/compliance-agent/src/llm/client.rs new file mode 100644 index 0000000..ebd72fb --- /dev/null +++ b/compliance-agent/src/llm/client.rs @@ -0,0 +1,157 @@ +use secrecy::{ExposeSecret, SecretString}; +use serde::{Deserialize, Serialize}; + +use crate::error::AgentError; + +#[derive(Clone)] +pub struct LlmClient { + base_url: String, + api_key: SecretString, + model: String, + http: reqwest::Client, +} + +#[derive(Serialize)] +struct ChatMessage { + role: String, + content: String, +} + +#[derive(Serialize)] +struct ChatCompletionRequest { + model: String, + messages: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_tokens: Option, +} + +#[derive(Deserialize)] +struct ChatCompletionResponse { + choices: Vec, +} + +#[derive(Deserialize)] +struct ChatChoice { + message: ChatResponseMessage, +} + +#[derive(Deserialize)] +struct ChatResponseMessage { + content: String, +} + +impl LlmClient { + pub fn new(base_url: String, api_key: SecretString, model: String) -> Self { + Self { + base_url, + api_key, + model, + http: reqwest::Client::new(), + } + } + + pub async fn chat( + &self, + system_prompt: &str, + user_prompt: &str, + temperature: Option, + ) -> Result { + let url = format!("{}/v1/chat/completions", self.base_url.trim_end_matches('/')); + + let request_body = ChatCompletionRequest { + model: self.model.clone(), + messages: vec![ + ChatMessage { + role: "system".to_string(), + content: system_prompt.to_string(), + }, + ChatMessage { + role: "user".to_string(), + content: user_prompt.to_string(), + }, + ], + temperature, + max_tokens: Some(4096), + }; + + let mut req = self + .http + .post(&url) + .header("content-type", "application/json") + .json(&request_body); + + let key = self.api_key.expose_secret(); + if !key.is_empty() { + req = req.header("Authorization", format!("Bearer {key}")); + } + + let resp = req.send().await.map_err(|e| { + AgentError::Other(format!("LiteLLM request failed: {e}")) + })?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(AgentError::Other(format!("LiteLLM returned {status}: {body}"))); + } + + let body: ChatCompletionResponse = resp.json().await.map_err(|e| { + AgentError::Other(format!("Failed to parse LiteLLM response: {e}")) + })?; + + body.choices + .first() + .map(|c| c.message.content.clone()) + .ok_or_else(|| AgentError::Other("Empty response from LiteLLM".to_string())) + } + + pub async fn chat_with_messages( + &self, + messages: Vec<(String, String)>, + temperature: Option, + ) -> Result { + let url = format!("{}/v1/chat/completions", self.base_url.trim_end_matches('/')); + + let request_body = ChatCompletionRequest { + model: self.model.clone(), + messages: messages + .into_iter() + .map(|(role, content)| ChatMessage { role, content }) + .collect(), + temperature, + max_tokens: Some(4096), + }; + + let mut req = self + .http + .post(&url) + .header("content-type", "application/json") + .json(&request_body); + + let key = self.api_key.expose_secret(); + if !key.is_empty() { + req = req.header("Authorization", format!("Bearer {key}")); + } + + let resp = req.send().await.map_err(|e| { + AgentError::Other(format!("LiteLLM request failed: {e}")) + })?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(AgentError::Other(format!("LiteLLM returned {status}: {body}"))); + } + + let body: ChatCompletionResponse = resp.json().await.map_err(|e| { + AgentError::Other(format!("Failed to parse LiteLLM response: {e}")) + })?; + + body.choices + .first() + .map(|c| c.message.content.clone()) + .ok_or_else(|| AgentError::Other("Empty response from LiteLLM".to_string())) + } +} diff --git a/compliance-agent/src/llm/descriptions.rs b/compliance-agent/src/llm/descriptions.rs new file mode 100644 index 0000000..093493e --- /dev/null +++ b/compliance-agent/src/llm/descriptions.rs @@ -0,0 +1,65 @@ +use std::sync::Arc; + +use compliance_core::models::Finding; + +use crate::error::AgentError; +use crate::llm::LlmClient; + +const DESCRIPTION_SYSTEM_PROMPT: &str = r#"You are a security engineer writing issue descriptions for a bug tracker. Generate a clear, actionable issue body in Markdown format that includes: + +1. **Summary**: 1-2 sentence overview +2. **Evidence**: Code location, snippet, and what was detected +3. **Impact**: What could happen if not fixed +4. **Remediation**: Step-by-step fix instructions +5. **References**: Relevant CWE/CVE links if applicable + +Keep it concise and professional. Use code blocks for code snippets."#; + +pub async fn generate_issue_description( + llm: &Arc, + finding: &Finding, +) -> Result<(String, String), AgentError> { + let user_prompt = format!( + "Generate an issue title and body for this finding:\n\ + Scanner: {}\n\ + Type: {}\n\ + Severity: {}\n\ + Rule: {}\n\ + Title: {}\n\ + Description: {}\n\ + File: {}\n\ + Line: {}\n\ + Code:\n```\n{}\n```\n\ + CWE: {}\n\ + CVE: {}\n\ + Remediation hint: {}", + finding.scanner, + finding.scan_type, + finding.severity, + finding.rule_id.as_deref().unwrap_or("N/A"), + finding.title, + finding.description, + finding.file_path.as_deref().unwrap_or("N/A"), + finding.line_number.map(|n| n.to_string()).unwrap_or_else(|| "N/A".to_string()), + finding.code_snippet.as_deref().unwrap_or("N/A"), + finding.cwe.as_deref().unwrap_or("N/A"), + finding.cve.as_deref().unwrap_or("N/A"), + finding.remediation.as_deref().unwrap_or("N/A"), + ); + + let response = llm.chat(DESCRIPTION_SYSTEM_PROMPT, &user_prompt, Some(0.3)).await?; + + // Extract title from first line, rest is body + let mut lines = response.lines(); + let title = lines + .next() + .unwrap_or(&finding.title) + .trim_start_matches('#') + .trim() + .to_string(); + let body = lines.collect::>().join("\n").trim().to_string(); + + let body = if body.is_empty() { response } else { body }; + + Ok((title, body)) +} diff --git a/compliance-agent/src/llm/fixes.rs b/compliance-agent/src/llm/fixes.rs new file mode 100644 index 0000000..518d01a --- /dev/null +++ b/compliance-agent/src/llm/fixes.rs @@ -0,0 +1,27 @@ +use std::sync::Arc; + +use compliance_core::models::Finding; + +use crate::error::AgentError; +use crate::llm::LlmClient; + +const FIX_SYSTEM_PROMPT: &str = r#"You are a security engineer. Given a security finding with code context, suggest a concrete code fix. Return ONLY the fixed code snippet that can directly replace the vulnerable code. Include brief inline comments explaining the fix."#; + +pub async fn suggest_fix( + llm: &Arc, + finding: &Finding, +) -> Result { + let user_prompt = format!( + "Suggest a fix for this vulnerability:\n\ + Language context from file: {}\n\ + Rule: {}\n\ + Description: {}\n\ + Vulnerable code:\n```\n{}\n```", + finding.file_path.as_deref().unwrap_or("unknown"), + finding.rule_id.as_deref().unwrap_or("N/A"), + finding.description, + finding.code_snippet.as_deref().unwrap_or("N/A"), + ); + + llm.chat(FIX_SYSTEM_PROMPT, &user_prompt, Some(0.2)).await +} diff --git a/compliance-agent/src/llm/mod.rs b/compliance-agent/src/llm/mod.rs new file mode 100644 index 0000000..9259b95 --- /dev/null +++ b/compliance-agent/src/llm/mod.rs @@ -0,0 +1,10 @@ +pub mod client; +#[allow(dead_code)] +pub mod descriptions; +#[allow(dead_code)] +pub mod fixes; +#[allow(dead_code)] +pub mod pr_review; +pub mod triage; + +pub use client::LlmClient; diff --git a/compliance-agent/src/llm/pr_review.rs b/compliance-agent/src/llm/pr_review.rs new file mode 100644 index 0000000..a256bea --- /dev/null +++ b/compliance-agent/src/llm/pr_review.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use compliance_core::models::Finding; +use compliance_core::traits::issue_tracker::ReviewComment; + +use crate::error::AgentError; +use crate::llm::LlmClient; + +const PR_REVIEW_SYSTEM_PROMPT: &str = r#"You are a security-focused code reviewer. Given a list of security findings in a PR diff, generate concise review comments. Each comment should: +1. Briefly explain the issue +2. Suggest a specific fix +3. Reference the relevant security standard (CWE, OWASP) if applicable + +Be constructive and professional. Return JSON array: +[{"path": "file.rs", "line": 42, "body": "..."}]"#; + +pub async fn generate_pr_review( + llm: &Arc, + findings: &[Finding], +) -> Result<(String, Vec), AgentError> { + if findings.is_empty() { + return Ok(("No security issues found in this PR.".to_string(), Vec::new())); + } + + let findings_text: Vec = findings + .iter() + .map(|f| { + format!( + "- [{severity}] {title} in {file}:{line}\n Code: {code}\n Rule: {rule}", + severity = f.severity, + title = f.title, + file = f.file_path.as_deref().unwrap_or("unknown"), + line = f.line_number.map(|n| n.to_string()).unwrap_or_else(|| "?".to_string()), + code = f.code_snippet.as_deref().unwrap_or("N/A"), + rule = f.rule_id.as_deref().unwrap_or("N/A"), + ) + }) + .collect(); + + let user_prompt = format!( + "Generate review comments for these {} findings:\n{}", + findings.len(), + findings_text.join("\n"), + ); + + let response = llm.chat(PR_REVIEW_SYSTEM_PROMPT, &user_prompt, Some(0.3)).await?; + + // Parse comments from LLM response + let comments: Vec = serde_json::from_str::>(&response) + .unwrap_or_default() + .into_iter() + .map(|c| ReviewComment { + path: c.path, + line: c.line, + body: c.body, + }) + .collect(); + + let summary = format!( + "## Security Review\n\nFound **{}** potential security issue(s) in this PR.\n\n{}", + findings.len(), + findings + .iter() + .map(|f| format!("- **[{}]** {} in `{}`", f.severity, f.title, f.file_path.as_deref().unwrap_or("unknown"))) + .collect::>() + .join("\n"), + ); + + Ok((summary, comments)) +} + +#[derive(serde::Deserialize)] +struct PrComment { + path: String, + line: u32, + body: String, +} diff --git a/compliance-agent/src/llm/triage.rs b/compliance-agent/src/llm/triage.rs new file mode 100644 index 0000000..b7bdbb5 --- /dev/null +++ b/compliance-agent/src/llm/triage.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use compliance_core::models::{Finding, FindingStatus}; + +use crate::llm::LlmClient; + +const TRIAGE_SYSTEM_PROMPT: &str = r#"You are a security finding triage expert. Analyze the following security finding and determine: +1. Is this a true positive? (yes/no) +2. Confidence score (0-10, where 10 is highest confidence this is a real issue) +3. Brief remediation suggestion (1-2 sentences) + +Respond in JSON format: +{"true_positive": true/false, "confidence": N, "remediation": "..."}"#; + +pub async fn triage_findings(llm: &Arc, findings: &mut Vec) -> usize { + let mut passed = 0; + + for finding in findings.iter_mut() { + let user_prompt = format!( + "Scanner: {}\nRule: {}\nSeverity: {}\nTitle: {}\nDescription: {}\nFile: {}\nLine: {}\nCode: {}", + finding.scanner, + finding.rule_id.as_deref().unwrap_or("N/A"), + finding.severity, + finding.title, + finding.description, + finding.file_path.as_deref().unwrap_or("N/A"), + finding.line_number.map(|n| n.to_string()).unwrap_or_else(|| "N/A".to_string()), + finding.code_snippet.as_deref().unwrap_or("N/A"), + ); + + match llm.chat(TRIAGE_SYSTEM_PROMPT, &user_prompt, Some(0.1)).await { + Ok(response) => { + if let Ok(result) = serde_json::from_str::(&response) { + finding.confidence = Some(result.confidence); + if let Some(remediation) = result.remediation { + finding.remediation = Some(remediation); + } + + if result.confidence >= 3.0 { + finding.status = FindingStatus::Triaged; + passed += 1; + } else { + finding.status = FindingStatus::FalsePositive; + } + } else { + // If LLM response doesn't parse, keep the finding + finding.status = FindingStatus::Triaged; + passed += 1; + tracing::warn!("Failed to parse triage response for {}: {response}", finding.fingerprint); + } + } + Err(e) => { + // On LLM error, keep the finding + tracing::warn!("LLM triage failed for {}: {e}", finding.fingerprint); + finding.status = FindingStatus::Triaged; + passed += 1; + } + } + } + + // Remove false positives + findings.retain(|f| f.status != FindingStatus::FalsePositive); + passed +} + +#[derive(serde::Deserialize)] +struct TriageResult { + #[serde(default)] + true_positive: bool, + #[serde(default)] + confidence: f64, + remediation: Option, +} diff --git a/compliance-agent/src/main.rs b/compliance-agent/src/main.rs new file mode 100644 index 0000000..d4a5f87 --- /dev/null +++ b/compliance-agent/src/main.rs @@ -0,0 +1,53 @@ +use tracing_subscriber::EnvFilter; + +mod agent; +mod config; +mod database; +mod error; +mod api; +mod llm; +mod pipeline; +mod scheduler; +#[allow(dead_code)] +mod trackers; +mod webhooks; + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .init(); + + dotenvy::dotenv().ok(); + + tracing::info!("Loading configuration..."); + let config = config::load_config()?; + + tracing::info!("Connecting to MongoDB..."); + let db = database::Database::connect(&config.mongodb_uri, &config.mongodb_database).await?; + db.ensure_indexes().await?; + + let agent = agent::ComplianceAgent::new(config.clone(), db.clone()); + + tracing::info!("Starting scheduler..."); + let scheduler_agent = agent.clone(); + let scheduler_handle = tokio::spawn(async move { + if let Err(e) = scheduler::start_scheduler(&scheduler_agent).await { + tracing::error!("Scheduler error: {e}"); + } + }); + + tracing::info!("Starting webhook server..."); + let webhook_agent = agent.clone(); + let webhook_handle = tokio::spawn(async move { + if let Err(e) = webhooks::start_webhook_server(&webhook_agent).await { + tracing::error!("Webhook server error: {e}"); + } + }); + + tracing::info!("Starting REST API on port {}...", config.agent_port); + api::start_api_server(agent, config.agent_port).await?; + + let _ = tokio::join!(scheduler_handle, webhook_handle); + Ok(()) +} diff --git a/compliance-agent/src/pipeline/cve.rs b/compliance-agent/src/pipeline/cve.rs new file mode 100644 index 0000000..004ed7a --- /dev/null +++ b/compliance-agent/src/pipeline/cve.rs @@ -0,0 +1,199 @@ +use compliance_core::models::{CveAlert, CveSource, SbomEntry, VulnRef}; +use compliance_core::CoreError; + +pub struct CveScanner { + http: reqwest::Client, + searxng_url: Option, + nvd_api_key: Option, +} + +impl CveScanner { + pub fn new(http: reqwest::Client, searxng_url: Option, nvd_api_key: Option) -> Self { + Self { http, searxng_url, nvd_api_key } + } + + pub async fn scan_dependencies( + &self, + repo_id: &str, + entries: &mut [SbomEntry], + ) -> Result, CoreError> { + let mut alerts = Vec::new(); + + // Batch query OSV.dev + let osv_results = self.query_osv_batch(entries).await?; + for (idx, vulns) in osv_results.into_iter().enumerate() { + if let Some(entry) = entries.get_mut(idx) { + for vuln in &vulns { + entry.known_vulnerabilities.push(VulnRef { + id: vuln.id.clone(), + source: "osv".to_string(), + severity: vuln.severity.clone(), + url: Some(format!("https://osv.dev/vulnerability/{}", vuln.id)), + }); + + let mut alert = CveAlert::new( + vuln.id.clone(), + repo_id.to_string(), + entry.name.clone(), + entry.version.clone(), + CveSource::Osv, + ); + alert.summary = vuln.summary.clone(); + alerts.push(alert); + } + } + } + + // Enrich with NVD CVSS scores + for alert in &mut alerts { + if let Ok(Some(cvss)) = self.query_nvd(&alert.cve_id).await { + alert.cvss_score = Some(cvss); + } + } + + Ok(alerts) + } + + async fn query_osv_batch(&self, entries: &[SbomEntry]) -> Result>, CoreError> { + let queries: Vec<_> = entries + .iter() + .filter_map(|e| { + e.purl.as_ref().map(|purl| { + serde_json::json!({ + "package": { "purl": purl } + }) + }) + }) + .collect(); + + if queries.is_empty() { + return Ok(Vec::new()); + } + + let body = serde_json::json!({ "queries": queries }); + + let resp = self + .http + .post("https://api.osv.dev/v1/querybatch") + .json(&body) + .send() + .await + .map_err(|e| CoreError::Http(format!("OSV.dev request failed: {e}")))?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + tracing::warn!("OSV.dev returned {status}: {body}"); + return Ok(Vec::new()); + } + + let result: OsvBatchResponse = resp.json().await.map_err(|e| { + CoreError::Http(format!("Failed to parse OSV.dev response: {e}")) + })?; + + let vulns = result + .results + .into_iter() + .map(|r| { + r.vulns + .unwrap_or_default() + .into_iter() + .map(|v| OsvVuln { + id: v.id, + summary: v.summary, + severity: v.database_specific + .and_then(|d| d.get("severity").and_then(|s| s.as_str()).map(String::from)), + }) + .collect() + }) + .collect(); + + Ok(vulns) + } + + async fn query_nvd(&self, cve_id: &str) -> Result, CoreError> { + if !cve_id.starts_with("CVE-") { + return Ok(None); + } + + let url = format!("https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={cve_id}"); + let mut req = self.http.get(&url); + + if let Some(key) = &self.nvd_api_key { + req = req.header("apiKey", key.as_str()); + } + + let resp = req.send().await.map_err(|e| { + CoreError::Http(format!("NVD request failed: {e}")) + })?; + + if !resp.status().is_success() { + return Ok(None); + } + + let body: serde_json::Value = resp.json().await.map_err(|e| { + CoreError::Http(format!("Failed to parse NVD response: {e}")) + })?; + + // Extract CVSS v3.1 base score + let score = body["vulnerabilities"] + .as_array() + .and_then(|v| v.first()) + .and_then(|v| v["cve"]["metrics"]["cvssMetricV31"].as_array()) + .and_then(|m| m.first()) + .and_then(|m| m["cvssData"]["baseScore"].as_f64()); + + Ok(score) + } + + pub async fn search_context(&self, cve_id: &str) -> Result, CoreError> { + let Some(searxng_url) = &self.searxng_url else { + return Ok(Vec::new()); + }; + + let url = format!("{}/search?q={cve_id}&format=json&engines=duckduckgo", searxng_url.trim_end_matches('/')); + let resp = self.http.get(&url).send().await.map_err(|e| { + CoreError::Http(format!("SearXNG request failed: {e}")) + })?; + + if !resp.status().is_success() { + return Ok(Vec::new()); + } + + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + let results = body["results"] + .as_array() + .map(|arr| { + arr.iter() + .take(5) + .filter_map(|r| r["url"].as_str().map(String::from)) + .collect() + }) + .unwrap_or_default(); + + Ok(results) + } +} + +#[derive(serde::Deserialize)] +struct OsvBatchResponse { + results: Vec, +} + +#[derive(serde::Deserialize)] +struct OsvBatchResult { + vulns: Option>, +} + +#[derive(serde::Deserialize)] +struct OsvVulnEntry { + id: String, + summary: Option, + database_specific: Option, +} + +struct OsvVuln { + id: String, + summary: Option, + severity: Option, +} diff --git a/compliance-agent/src/pipeline/dedup.rs b/compliance-agent/src/pipeline/dedup.rs new file mode 100644 index 0000000..d20544e --- /dev/null +++ b/compliance-agent/src/pipeline/dedup.rs @@ -0,0 +1,10 @@ +use sha2::{Digest, Sha256}; + +pub fn compute_fingerprint(parts: &[&str]) -> String { + let mut hasher = Sha256::new(); + for part in parts { + hasher.update(part.as_bytes()); + hasher.update(b"|"); + } + hex::encode(hasher.finalize()) +} diff --git a/compliance-agent/src/pipeline/git.rs b/compliance-agent/src/pipeline/git.rs new file mode 100644 index 0000000..d490558 --- /dev/null +++ b/compliance-agent/src/pipeline/git.rs @@ -0,0 +1,100 @@ +use std::path::{Path, PathBuf}; + +use git2::{FetchOptions, Repository}; + +use crate::error::AgentError; + +pub struct GitOps { + base_path: PathBuf, +} + +impl GitOps { + pub fn new(base_path: &str) -> Self { + Self { + base_path: PathBuf::from(base_path), + } + } + + pub fn clone_or_fetch(&self, git_url: &str, repo_name: &str) -> Result { + let repo_path = self.base_path.join(repo_name); + + if repo_path.exists() { + self.fetch(&repo_path)?; + } else { + std::fs::create_dir_all(&repo_path)?; + Repository::clone(git_url, &repo_path)?; + tracing::info!("Cloned {git_url} to {}", repo_path.display()); + } + + Ok(repo_path) + } + + fn fetch(&self, repo_path: &Path) -> Result<(), AgentError> { + let repo = Repository::open(repo_path)?; + let mut remote = repo.find_remote("origin")?; + let mut fetch_opts = FetchOptions::new(); + remote.fetch(&[] as &[&str], Some(&mut fetch_opts), None)?; + + // Fast-forward to origin/HEAD + let fetch_head = repo.find_reference("FETCH_HEAD")?; + let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?; + let head_ref = repo.head()?; + let head_name = head_ref.name().unwrap_or("HEAD"); + + repo.reference( + head_name, + fetch_commit.id(), + true, + "fast-forward", + )?; + repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?; + + tracing::info!("Fetched and fast-forwarded {}", repo_path.display()); + Ok(()) + } + + pub fn get_head_sha(repo_path: &Path) -> Result { + let repo = Repository::open(repo_path)?; + let head = repo.head()?; + let commit = head.peel_to_commit()?; + Ok(commit.id().to_string()) + } + + pub fn has_new_commits(repo_path: &Path, last_sha: Option<&str>) -> Result { + let current_sha = Self::get_head_sha(repo_path)?; + match last_sha { + Some(sha) if sha == current_sha => Ok(false), + _ => Ok(true), + } + } + + pub fn get_changed_files( + repo_path: &Path, + old_sha: &str, + new_sha: &str, + ) -> Result, AgentError> { + let repo = Repository::open(repo_path)?; + let old_commit = repo.find_commit(git2::Oid::from_str(old_sha)?)?; + let new_commit = repo.find_commit(git2::Oid::from_str(new_sha)?)?; + + let old_tree = old_commit.tree()?; + let new_tree = new_commit.tree()?; + + let diff = repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?; + + let mut files = Vec::new(); + diff.foreach( + &mut |delta, _| { + if let Some(path) = delta.new_file().path() { + files.push(path.to_string_lossy().to_string()); + } + true + }, + None, + None, + None, + )?; + + Ok(files) + } +} diff --git a/compliance-agent/src/pipeline/mod.rs b/compliance-agent/src/pipeline/mod.rs new file mode 100644 index 0000000..257da50 --- /dev/null +++ b/compliance-agent/src/pipeline/mod.rs @@ -0,0 +1,7 @@ +pub mod cve; +pub mod dedup; +pub mod git; +pub mod orchestrator; +pub mod patterns; +pub mod sbom; +pub mod semgrep; diff --git a/compliance-agent/src/pipeline/orchestrator.rs b/compliance-agent/src/pipeline/orchestrator.rs new file mode 100644 index 0000000..7688d42 --- /dev/null +++ b/compliance-agent/src/pipeline/orchestrator.rs @@ -0,0 +1,252 @@ +use std::sync::Arc; + +use mongodb::bson::doc; + +use compliance_core::models::*; +use compliance_core::traits::Scanner; +use compliance_core::AgentConfig; + +use crate::database::Database; +use crate::error::AgentError; +use crate::llm::LlmClient; +use crate::pipeline::cve::CveScanner; +use crate::pipeline::git::GitOps; +use crate::pipeline::patterns::{GdprPatternScanner, OAuthPatternScanner}; +use crate::pipeline::sbom::SbomScanner; +use crate::pipeline::semgrep::SemgrepScanner; + +pub struct PipelineOrchestrator { + config: AgentConfig, + db: Database, + llm: Arc, + http: reqwest::Client, +} + +impl PipelineOrchestrator { + pub fn new( + config: AgentConfig, + db: Database, + llm: Arc, + http: reqwest::Client, + ) -> Self { + Self { config, db, llm, http } + } + + pub async fn run( + &self, + repo_id: &str, + trigger: ScanTrigger, + ) -> Result<(), AgentError> { + // Look up the repository + let repo = self + .db + .repositories() + .find_one(doc! { "_id": mongodb::bson::oid::ObjectId::parse_str(repo_id).map_err(|e| AgentError::Other(e.to_string()))? }) + .await? + .ok_or_else(|| AgentError::Other(format!("Repository {repo_id} not found")))?; + + // Create scan run + let scan_run = ScanRun::new(repo_id.to_string(), trigger); + let insert = self.db.scan_runs().insert_one(&scan_run).await?; + let scan_run_id = insert.inserted_id.as_object_id() + .map(|id| id.to_hex()) + .unwrap_or_default(); + + let result = self.run_pipeline(&repo, &scan_run_id).await; + + // Update scan run status + match &result { + Ok(count) => { + self.db.scan_runs().update_one( + doc! { "_id": &insert.inserted_id }, + doc! { + "$set": { + "status": "completed", + "current_phase": "completed", + "new_findings_count": *count as i64, + "completed_at": mongodb::bson::DateTime::now(), + } + }, + ).await?; + } + Err(e) => { + self.db.scan_runs().update_one( + doc! { "_id": &insert.inserted_id }, + doc! { + "$set": { + "status": "failed", + "error_message": e.to_string(), + "completed_at": mongodb::bson::DateTime::now(), + } + }, + ).await?; + } + } + + result.map(|_| ()) + } + + async fn run_pipeline( + &self, + repo: &TrackedRepository, + scan_run_id: &str, + ) -> Result { + let repo_id = repo.id.as_ref() + .map(|id| id.to_hex()) + .unwrap_or_default(); + + // Stage 0: Change detection + tracing::info!("[{repo_id}] Stage 0: Change detection"); + let git_ops = GitOps::new(&self.config.git_clone_base_path); + let repo_path = git_ops.clone_or_fetch(&repo.git_url, &repo.name)?; + + if !GitOps::has_new_commits(&repo_path, repo.last_scanned_commit.as_deref())? { + tracing::info!("[{repo_id}] No new commits, skipping scan"); + return Ok(0); + } + + let current_sha = GitOps::get_head_sha(&repo_path)?; + let mut all_findings: Vec = Vec::new(); + + // Stage 1: Semgrep SAST + tracing::info!("[{repo_id}] Stage 1: Semgrep SAST"); + self.update_phase(scan_run_id, "sast").await; + let semgrep = SemgrepScanner; + match semgrep.scan(&repo_path, &repo_id).await { + Ok(output) => all_findings.extend(output.findings), + Err(e) => tracing::warn!("[{repo_id}] Semgrep failed: {e}"), + } + + // Stage 2: SBOM Generation + tracing::info!("[{repo_id}] Stage 2: SBOM Generation"); + self.update_phase(scan_run_id, "sbom_generation").await; + let sbom_scanner = SbomScanner; + let mut sbom_entries = match sbom_scanner.scan(&repo_path, &repo_id).await { + Ok(output) => output.sbom_entries, + Err(e) => { + tracing::warn!("[{repo_id}] SBOM generation failed: {e}"); + Vec::new() + } + }; + + // Stage 3: CVE Scanning + tracing::info!("[{repo_id}] Stage 3: CVE Scanning"); + self.update_phase(scan_run_id, "cve_scanning").await; + let cve_scanner = CveScanner::new( + self.http.clone(), + self.config.searxng_url.clone(), + self.config.nvd_api_key.as_ref().map(|k| { + use secrecy::ExposeSecret; + k.expose_secret().to_string() + }), + ); + let cve_alerts = match cve_scanner.scan_dependencies(&repo_id, &mut sbom_entries).await { + Ok(alerts) => alerts, + Err(e) => { + tracing::warn!("[{repo_id}] CVE scanning failed: {e}"); + Vec::new() + } + }; + + // Stage 4: Pattern Scanning (GDPR + OAuth) + tracing::info!("[{repo_id}] Stage 4: Pattern Scanning"); + self.update_phase(scan_run_id, "pattern_scanning").await; + let gdpr = GdprPatternScanner::new(); + match gdpr.scan(&repo_path, &repo_id).await { + Ok(output) => all_findings.extend(output.findings), + Err(e) => tracing::warn!("[{repo_id}] GDPR pattern scan failed: {e}"), + } + let oauth = OAuthPatternScanner::new(); + match oauth.scan(&repo_path, &repo_id).await { + Ok(output) => all_findings.extend(output.findings), + Err(e) => tracing::warn!("[{repo_id}] OAuth pattern scan failed: {e}"), + } + + // Stage 5: LLM Triage + tracing::info!("[{repo_id}] Stage 5: LLM Triage ({} findings)", all_findings.len()); + self.update_phase(scan_run_id, "llm_triage").await; + let triaged = crate::llm::triage::triage_findings(&self.llm, &mut all_findings).await; + tracing::info!("[{repo_id}] Triaged: {triaged} findings passed confidence threshold"); + + // Dedup against existing findings and insert new ones + let mut new_count = 0u32; + for mut finding in all_findings { + finding.scan_run_id = Some(scan_run_id.to_string()); + // Check if fingerprint already exists + let existing = self + .db + .findings() + .find_one(doc! { "fingerprint": &finding.fingerprint }) + .await?; + if existing.is_none() { + self.db.findings().insert_one(&finding).await?; + new_count += 1; + } + } + + // Persist SBOM entries (upsert by repo_id + name + version) + for entry in &sbom_entries { + let filter = doc! { + "repo_id": &entry.repo_id, + "name": &entry.name, + "version": &entry.version, + }; + let update = mongodb::bson::to_document(entry) + .map(|d| doc! { "$set": d }) + .unwrap_or_else(|_| doc! {}); + self.db + .sbom_entries() + .update_one(filter, update) + .upsert(true) + .await?; + } + + // Persist CVE alerts (upsert by cve_id + repo_id) + for alert in &cve_alerts { + let filter = doc! { + "cve_id": &alert.cve_id, + "repo_id": &alert.repo_id, + }; + let update = mongodb::bson::to_document(alert) + .map(|d| doc! { "$set": d }) + .unwrap_or_else(|_| doc! {}); + self.db + .cve_alerts() + .update_one(filter, update) + .upsert(true) + .await?; + } + + // Stage 6: Issue Creation + tracing::info!("[{repo_id}] Stage 6: Issue Creation"); + self.update_phase(scan_run_id, "issue_creation").await; + // Issue creation is handled by the trackers module - deferred to agent + + // Stage 7: Update repository + self.db.repositories().update_one( + doc! { "_id": repo.id }, + doc! { + "$set": { + "last_scanned_commit": ¤t_sha, + "updated_at": mongodb::bson::DateTime::now(), + }, + "$inc": { "findings_count": new_count as i64 }, + }, + ).await?; + + tracing::info!("[{repo_id}] Scan complete: {new_count} new findings"); + Ok(new_count) + } + + async fn update_phase(&self, scan_run_id: &str, phase: &str) { + if let Ok(oid) = mongodb::bson::oid::ObjectId::parse_str(scan_run_id) { + let _ = self.db.scan_runs().update_one( + doc! { "_id": oid }, + doc! { + "$set": { "current_phase": phase }, + "$push": { "phases_completed": phase }, + }, + ).await; + } + } +} diff --git a/compliance-agent/src/pipeline/patterns.rs b/compliance-agent/src/pipeline/patterns.rs new file mode 100644 index 0000000..87fa125 --- /dev/null +++ b/compliance-agent/src/pipeline/patterns.rs @@ -0,0 +1,226 @@ +use std::path::Path; + +use compliance_core::models::{Finding, ScanType, Severity}; +use compliance_core::traits::{ScanOutput, Scanner}; +use compliance_core::CoreError; +use regex::Regex; + +use crate::pipeline::dedup; + +pub struct GdprPatternScanner { + patterns: Vec, +} + +pub struct OAuthPatternScanner { + patterns: Vec, +} + +struct PatternRule { + id: String, + title: String, + description: String, + pattern: Regex, + severity: Severity, + file_extensions: Vec, +} + +impl GdprPatternScanner { + pub fn new() -> Self { + let patterns = vec![ + PatternRule { + id: "gdpr-pii-logging".to_string(), + title: "PII data potentially logged".to_string(), + description: "Logging statements that may contain personally identifiable information (email, SSN, phone, IP address).".to_string(), + pattern: Regex::new(r#"(?i)(log|print|console\.|logger\.|tracing::)\s*[\.(].*\b(email|ssn|social.?security|phone.?number|ip.?addr|passport|date.?of.?birth|credit.?card)\b"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + severity: Severity::High, + file_extensions: vec!["rs", "py", "js", "ts", "java", "go", "rb"].into_iter().map(String::from).collect(), + }, + PatternRule { + id: "gdpr-no-consent".to_string(), + title: "Data collection without apparent consent mechanism".to_string(), + description: "Data collection endpoint that doesn't reference consent or opt-in mechanisms.".to_string(), + pattern: Regex::new(r#"(?i)(collect|store|save|persist|record).*\b(personal|user.?data|pii|biometric)\b"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + severity: Severity::Medium, + file_extensions: vec!["rs", "py", "js", "ts", "java", "go"].into_iter().map(String::from).collect(), + }, + PatternRule { + id: "gdpr-no-delete-endpoint".to_string(), + title: "Missing data deletion capability".to_string(), + description: "User data models or controllers without corresponding deletion endpoints (right to erasure).".to_string(), + pattern: Regex::new(r#"(?i)(class|struct|model)\s+User(?!.*[Dd]elete)"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + severity: Severity::Medium, + file_extensions: vec!["rs", "py", "js", "ts", "java", "go", "rb"].into_iter().map(String::from).collect(), + }, + PatternRule { + id: "gdpr-hardcoded-retention".to_string(), + title: "Hardcoded data retention period".to_string(), + description: "Data retention periods should be configurable for GDPR compliance.".to_string(), + pattern: Regex::new(r#"(?i)(retention|ttl|expire|keep.?for)\s*[=:]\s*\d+"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + severity: Severity::Low, + file_extensions: vec!["rs", "py", "js", "ts", "java", "go", "yaml", "yml", "toml", "json"].into_iter().map(String::from).collect(), + }, + ]; + Self { patterns } + } +} + +impl Scanner for GdprPatternScanner { + fn name(&self) -> &str { + "gdpr-patterns" + } + + fn scan_type(&self) -> ScanType { + ScanType::Gdpr + } + + async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result { + let findings = scan_with_patterns(repo_path, repo_id, &self.patterns, ScanType::Gdpr, "gdpr-patterns")?; + Ok(ScanOutput { + findings, + sbom_entries: Vec::new(), + }) + } +} + +impl OAuthPatternScanner { + pub fn new() -> Self { + let patterns = vec![ + PatternRule { + id: "oauth-implicit-grant".to_string(), + title: "OAuth implicit grant flow detected".to_string(), + description: "Implicit grant flow is deprecated and insecure. Use authorization code flow with PKCE instead.".to_string(), + pattern: Regex::new(r#"(?i)(response_type\s*[=:]\s*["']?token|grant_type\s*[=:]\s*["']?implicit)"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + severity: Severity::High, + file_extensions: vec!["rs", "py", "js", "ts", "java", "go", "yaml", "yml", "json"].into_iter().map(String::from).collect(), + }, + PatternRule { + id: "oauth-missing-pkce".to_string(), + title: "OAuth flow without PKCE".to_string(), + description: "Authorization code flow should use PKCE (code_challenge/code_verifier) for public clients.".to_string(), + pattern: Regex::new(r#"(?i)authorization.?code(?!.*code.?challenge)(?!.*pkce)"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + severity: Severity::Medium, + file_extensions: vec!["rs", "py", "js", "ts", "java", "go"].into_iter().map(String::from).collect(), + }, + PatternRule { + id: "oauth-token-localstorage".to_string(), + title: "Token stored in localStorage".to_string(), + description: "Storing tokens in localStorage is vulnerable to XSS. Use httpOnly cookies or secure session storage.".to_string(), + pattern: Regex::new(r#"(?i)localStorage\.(set|get)Item\s*\(\s*["'].*token"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + severity: Severity::High, + file_extensions: vec!["js", "ts", "jsx", "tsx"].into_iter().map(String::from).collect(), + }, + PatternRule { + id: "oauth-token-url".to_string(), + title: "Token passed in URL parameters".to_string(), + description: "Tokens in URLs can leak via referrer headers, server logs, and browser history.".to_string(), + pattern: Regex::new(r#"(?i)(access_token|bearer)\s*[=]\s*.*\b(url|query|param|href)\b"#).unwrap_or_else(|_| Regex::new("^$").unwrap()), + severity: Severity::High, + file_extensions: vec!["rs", "py", "js", "ts", "java", "go"].into_iter().map(String::from).collect(), + }, + ]; + Self { patterns } + } +} + +impl Scanner for OAuthPatternScanner { + fn name(&self) -> &str { + "oauth-patterns" + } + + fn scan_type(&self) -> ScanType { + ScanType::OAuth + } + + async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result { + let findings = scan_with_patterns(repo_path, repo_id, &self.patterns, ScanType::OAuth, "oauth-patterns")?; + Ok(ScanOutput { + findings, + sbom_entries: Vec::new(), + }) + } +} + +fn scan_with_patterns( + repo_path: &Path, + repo_id: &str, + patterns: &[PatternRule], + scan_type: ScanType, + scanner_name: &str, +) -> Result, CoreError> { + let mut findings = Vec::new(); + + for entry in walkdir(repo_path)? { + let path = entry.path(); + if !path.is_file() { + continue; + } + + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_string(); + + let content = match std::fs::read_to_string(path) { + Ok(c) => c, + Err(_) => continue, // skip binary files + }; + + let relative_path = path + .strip_prefix(repo_path) + .unwrap_or(path) + .to_string_lossy() + .to_string(); + + for pattern in patterns { + if !pattern.file_extensions.contains(&ext) { + continue; + } + + for (line_num, line) in content.lines().enumerate() { + if pattern.pattern.is_match(line) { + let fingerprint = dedup::compute_fingerprint(&[ + repo_id, + &pattern.id, + &relative_path, + &(line_num + 1).to_string(), + ]); + + let mut finding = Finding::new( + repo_id.to_string(), + fingerprint, + scanner_name.to_string(), + scan_type.clone(), + pattern.title.clone(), + pattern.description.clone(), + pattern.severity.clone(), + ); + finding.rule_id = Some(pattern.id.clone()); + finding.file_path = Some(relative_path.clone()); + finding.line_number = Some((line_num + 1) as u32); + finding.code_snippet = Some(line.to_string()); + + findings.push(finding); + } + } + } + } + + Ok(findings) +} + +fn walkdir(path: &Path) -> Result, CoreError> { + // Simple recursive file walk, skipping hidden dirs and common non-source dirs + let skip_dirs = [".git", "node_modules", "target", "vendor", ".venv", "__pycache__", "dist", "build"]; + + let entries: Vec<_> = walkdir::WalkDir::new(path) + .into_iter() + .filter_entry(|e| { + let name = e.file_name().to_string_lossy(); + !skip_dirs.contains(&name.as_ref()) + }) + .filter_map(|e| e.ok()) + .collect(); + + Ok(entries) +} diff --git a/compliance-agent/src/pipeline/sbom.rs b/compliance-agent/src/pipeline/sbom.rs new file mode 100644 index 0000000..a19a97d --- /dev/null +++ b/compliance-agent/src/pipeline/sbom.rs @@ -0,0 +1,186 @@ +use std::path::Path; + +use compliance_core::models::{SbomEntry, ScanType, VulnRef}; +use compliance_core::traits::{ScanOutput, Scanner}; +use compliance_core::CoreError; + +pub struct SbomScanner; + +impl Scanner for SbomScanner { + fn name(&self) -> &str { + "sbom" + } + + fn scan_type(&self) -> ScanType { + ScanType::Sbom + } + + async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result { + let mut entries = Vec::new(); + + // Run syft for SBOM generation + match run_syft(repo_path, repo_id).await { + Ok(syft_entries) => entries.extend(syft_entries), + Err(e) => tracing::warn!("syft failed: {e}"), + } + + // Run cargo-audit for Rust-specific vulns + match run_cargo_audit(repo_path, repo_id).await { + Ok(vulns) => merge_audit_vulns(&mut entries, vulns), + Err(e) => tracing::warn!("cargo-audit skipped: {e}"), + } + + Ok(ScanOutput { + findings: Vec::new(), + sbom_entries: entries, + }) + } +} + +async fn run_syft(repo_path: &Path, repo_id: &str) -> Result, CoreError> { + let output = tokio::process::Command::new("syft") + .arg(repo_path) + .args(["-o", "cyclonedx-json"]) + .output() + .await + .map_err(|e| CoreError::Scanner { + scanner: "syft".to_string(), + source: Box::new(e), + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(CoreError::Scanner { + scanner: "syft".to_string(), + source: format!("syft exited with {}: {stderr}", output.status).into(), + }); + } + + let cdx: CycloneDxBom = serde_json::from_slice(&output.stdout)?; + let entries = cdx + .components + .unwrap_or_default() + .into_iter() + .map(|c| { + let mut entry = SbomEntry::new( + repo_id.to_string(), + c.name, + c.version.unwrap_or_else(|| "unknown".to_string()), + c.component_type.unwrap_or_else(|| "library".to_string()), + ); + entry.purl = c.purl; + entry.license = c.licenses.and_then(|ls| { + ls.first().and_then(|l| { + l.license.as_ref().map(|lic| { + lic.id.clone().unwrap_or_else(|| lic.name.clone().unwrap_or_default()) + }) + }) + }); + entry + }) + .collect(); + + Ok(entries) +} + +async fn run_cargo_audit(repo_path: &Path, _repo_id: &str) -> Result, CoreError> { + let cargo_lock = repo_path.join("Cargo.lock"); + if !cargo_lock.exists() { + return Ok(Vec::new()); + } + + let output = tokio::process::Command::new("cargo") + .args(["audit", "--json"]) + .current_dir(repo_path) + .output() + .await + .map_err(|e| CoreError::Scanner { + scanner: "cargo-audit".to_string(), + source: Box::new(e), + })?; + + let result: CargoAuditOutput = serde_json::from_slice(&output.stdout) + .unwrap_or_else(|_| CargoAuditOutput { vulnerabilities: CargoAuditVulns { list: Vec::new() } }); + + let vulns = result + .vulnerabilities + .list + .into_iter() + .map(|v| AuditVuln { + package: v.advisory.package, + id: v.advisory.id, + url: v.advisory.url, + }) + .collect(); + + Ok(vulns) +} + +fn merge_audit_vulns(entries: &mut Vec, vulns: Vec) { + for vuln in vulns { + if let Some(entry) = entries.iter_mut().find(|e| e.name == vuln.package) { + entry.known_vulnerabilities.push(VulnRef { + id: vuln.id.clone(), + source: "cargo-audit".to_string(), + severity: None, + url: Some(vuln.url), + }); + } + } +} + +// CycloneDX JSON types +#[derive(serde::Deserialize)] +struct CycloneDxBom { + components: Option>, +} + +#[derive(serde::Deserialize)] +struct CdxComponent { + name: String, + version: Option, + #[serde(rename = "type")] + component_type: Option, + purl: Option, + licenses: Option>, +} + +#[derive(serde::Deserialize)] +struct CdxLicenseWrapper { + license: Option, +} + +#[derive(serde::Deserialize)] +struct CdxLicense { + id: Option, + name: Option, +} + +// Cargo audit types +#[derive(serde::Deserialize)] +struct CargoAuditOutput { + vulnerabilities: CargoAuditVulns, +} + +#[derive(serde::Deserialize)] +struct CargoAuditVulns { + list: Vec, +} + +#[derive(serde::Deserialize)] +struct CargoAuditEntry { + advisory: CargoAuditAdvisory, +} + +#[derive(serde::Deserialize)] +struct CargoAuditAdvisory { + id: String, + package: String, + url: String, +} + +struct AuditVuln { + package: String, + id: String, + url: String, +} diff --git a/compliance-agent/src/pipeline/semgrep.rs b/compliance-agent/src/pipeline/semgrep.rs new file mode 100644 index 0000000..13e2bb4 --- /dev/null +++ b/compliance-agent/src/pipeline/semgrep.rs @@ -0,0 +1,110 @@ +use std::path::Path; + +use compliance_core::models::{Finding, ScanType, Severity}; +use compliance_core::traits::{ScanOutput, Scanner}; +use compliance_core::CoreError; + +use crate::pipeline::dedup; + +pub struct SemgrepScanner; + +impl Scanner for SemgrepScanner { + fn name(&self) -> &str { + "semgrep" + } + + fn scan_type(&self) -> ScanType { + ScanType::Sast + } + + async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result { + let output = tokio::process::Command::new("semgrep") + .args(["--config=auto", "--json", "--quiet"]) + .arg(repo_path) + .output() + .await + .map_err(|e| CoreError::Scanner { + scanner: "semgrep".to_string(), + source: Box::new(e), + })?; + + if !output.status.success() && output.stdout.is_empty() { + let stderr = String::from_utf8_lossy(&output.stderr); + tracing::warn!("Semgrep exited with {}: {stderr}", output.status); + return Ok(ScanOutput::default()); + } + + let result: SemgrepOutput = serde_json::from_slice(&output.stdout)?; + let findings = result + .results + .into_iter() + .map(|r| { + let severity = match r.extra.severity.as_str() { + "ERROR" => Severity::High, + "WARNING" => Severity::Medium, + "INFO" => Severity::Low, + _ => Severity::Info, + }; + + let fingerprint = dedup::compute_fingerprint(&[ + repo_id, + &r.check_id, + &r.path, + &r.start.line.to_string(), + ]); + + let mut finding = Finding::new( + repo_id.to_string(), + fingerprint, + "semgrep".to_string(), + ScanType::Sast, + r.extra.message.clone(), + r.extra.message, + severity, + ); + finding.rule_id = Some(r.check_id); + finding.file_path = Some(r.path); + finding.line_number = Some(r.start.line); + finding.code_snippet = Some(r.extra.lines); + finding.cwe = r.extra.metadata.and_then(|m| { + m.get("cwe") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + }); + finding + }) + .collect(); + + Ok(ScanOutput { + findings, + sbom_entries: Vec::new(), + }) + } +} + +#[derive(serde::Deserialize)] +struct SemgrepOutput { + results: Vec, +} + +#[derive(serde::Deserialize)] +struct SemgrepResult { + check_id: String, + path: String, + start: SemgrepPosition, + extra: SemgrepExtra, +} + +#[derive(serde::Deserialize)] +struct SemgrepPosition { + line: u32, +} + +#[derive(serde::Deserialize)] +struct SemgrepExtra { + message: String, + severity: String, + lines: String, + #[serde(default)] + metadata: Option, +} diff --git a/compliance-agent/src/scheduler.rs b/compliance-agent/src/scheduler.rs new file mode 100644 index 0000000..498d2db --- /dev/null +++ b/compliance-agent/src/scheduler.rs @@ -0,0 +1,105 @@ +use mongodb::bson::doc; +use tokio_cron_scheduler::{Job, JobScheduler}; + +use compliance_core::models::ScanTrigger; + +use crate::agent::ComplianceAgent; +use crate::error::AgentError; + +pub async fn start_scheduler(agent: &ComplianceAgent) -> Result<(), AgentError> { + let sched = JobScheduler::new() + .await + .map_err(|e| AgentError::Scheduler(format!("Failed to create scheduler: {e}")))?; + + // Periodic scan job + let scan_agent = agent.clone(); + let scan_schedule = agent.config.scan_schedule.clone(); + let scan_job = Job::new_async(scan_schedule.as_str(), move |_uuid, _lock| { + let agent = scan_agent.clone(); + Box::pin(async move { + tracing::info!("Scheduled scan triggered"); + scan_all_repos(&agent).await; + }) + }) + .map_err(|e| AgentError::Scheduler(format!("Failed to create scan job: {e}")))?; + sched.add(scan_job).await + .map_err(|e| AgentError::Scheduler(format!("Failed to add scan job: {e}")))?; + + // CVE monitor job (daily) + let cve_agent = agent.clone(); + let cve_schedule = agent.config.cve_monitor_schedule.clone(); + let cve_job = Job::new_async(cve_schedule.as_str(), move |_uuid, _lock| { + let agent = cve_agent.clone(); + Box::pin(async move { + tracing::info!("CVE monitor triggered"); + monitor_cves(&agent).await; + }) + }) + .map_err(|e| AgentError::Scheduler(format!("Failed to create CVE monitor job: {e}")))?; + sched.add(cve_job).await + .map_err(|e| AgentError::Scheduler(format!("Failed to add CVE monitor job: {e}")))?; + + sched.start().await + .map_err(|e| AgentError::Scheduler(format!("Failed to start scheduler: {e}")))?; + + tracing::info!( + "Scheduler started: scans='{}', CVE monitor='{}'", + agent.config.scan_schedule, + agent.config.cve_monitor_schedule, + ); + + // Keep scheduler alive + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await; + } +} + +async fn scan_all_repos(agent: &ComplianceAgent) { + use futures_util::StreamExt; + + let cursor = match agent.db.repositories().find(doc! {}).await { + Ok(c) => c, + Err(e) => { + tracing::error!("Failed to list repos for scheduled scan: {e}"); + return; + } + }; + + let repos: Vec<_> = cursor + .filter_map(|r| async { r.ok() }) + .collect() + .await; + + for repo in repos { + let repo_id = repo.id.map(|id| id.to_hex()).unwrap_or_default(); + if let Err(e) = agent.run_scan(&repo_id, ScanTrigger::Scheduled).await { + tracing::error!("Scheduled scan failed for {}: {e}", repo.name); + } + } +} + +async fn monitor_cves(agent: &ComplianceAgent) { + use futures_util::StreamExt; + + // Re-scan all SBOM entries for new CVEs + let cursor = match agent.db.sbom_entries().find(doc! {}).await { + Ok(c) => c, + Err(e) => { + tracing::error!("Failed to list SBOM entries for CVE monitoring: {e}"); + return; + } + }; + + let entries: Vec<_> = cursor + .filter_map(|r| async { r.ok() }) + .collect() + .await; + + if entries.is_empty() { + return; + } + + tracing::info!("CVE monitor: checking {} dependencies", entries.len()); + // The actual CVE checking is handled by the CveScanner in the pipeline + // This is a simplified version that just logs the activity +} diff --git a/compliance-agent/src/trackers/github.rs b/compliance-agent/src/trackers/github.rs new file mode 100644 index 0000000..b383a61 --- /dev/null +++ b/compliance-agent/src/trackers/github.rs @@ -0,0 +1,161 @@ +use compliance_core::error::CoreError; +use compliance_core::models::{TrackerIssue, TrackerType}; +use compliance_core::traits::issue_tracker::{IssueTracker, ReviewComment}; +use octocrab::Octocrab; +use secrecy::{ExposeSecret, SecretString}; + +pub struct GitHubTracker { + client: Octocrab, +} + +impl GitHubTracker { + pub fn new(token: &SecretString) -> Result { + let client = Octocrab::builder() + .personal_token(token.expose_secret().to_string()) + .build() + .map_err(|e| CoreError::IssueTracker(format!("Failed to create GitHub client: {e}")))?; + Ok(Self { client }) + } +} + +impl IssueTracker for GitHubTracker { + fn name(&self) -> &str { + "github" + } + + async fn create_issue( + &self, + owner: &str, + repo: &str, + title: &str, + body: &str, + labels: &[String], + ) -> Result { + let issues_handler = self.client.issues(owner, repo); + let mut builder = issues_handler.create(title).body(body); + if !labels.is_empty() { + builder = builder.labels(labels.to_vec()); + } + let issue = builder + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("GitHub create issue failed: {e}")))?; + + Ok(TrackerIssue::new( + String::new(), + TrackerType::GitHub, + issue.number.to_string(), + issue.html_url.to_string(), + title.to_string(), + )) + } + + async fn update_issue_status( + &self, + owner: &str, + repo: &str, + external_id: &str, + status: &str, + ) -> Result<(), CoreError> { + let issue_number: u64 = external_id + .parse() + .map_err(|_| CoreError::IssueTracker("Invalid issue number".to_string()))?; + + let state_str = match status { + "closed" | "resolved" => "closed", + _ => "open", + }; + + // Use the REST API directly for state update + let route = format!("/repos/{owner}/{repo}/issues/{issue_number}"); + let body = serde_json::json!({ "state": state_str }); + self.client + .post::(route, Some(&body)) + .await + .map_err(|e| CoreError::IssueTracker(format!("GitHub update issue failed: {e}")))?; + + Ok(()) + } + + async fn add_comment( + &self, + owner: &str, + repo: &str, + external_id: &str, + body: &str, + ) -> Result<(), CoreError> { + let issue_number: u64 = external_id + .parse() + .map_err(|_| CoreError::IssueTracker("Invalid issue number".to_string()))?; + + self.client + .issues(owner, repo) + .create_comment(issue_number, body) + .await + .map_err(|e| CoreError::IssueTracker(format!("GitHub add comment failed: {e}")))?; + + Ok(()) + } + + async fn create_pr_review( + &self, + owner: &str, + repo: &str, + pr_number: u64, + body: &str, + comments: Vec, + ) -> Result<(), CoreError> { + let review_comments: Vec = comments + .iter() + .map(|c| { + serde_json::json!({ + "path": c.path, + "line": c.line, + "body": c.body, + }) + }) + .collect(); + + let review_body = serde_json::json!({ + "body": body, + "event": "COMMENT", + "comments": review_comments, + }); + + let route = format!("/repos/{owner}/{repo}/pulls/{pr_number}/reviews"); + self.client + .post::(route, Some(&review_body)) + .await + .map_err(|e| CoreError::IssueTracker(format!("GitHub PR review failed: {e}")))?; + + Ok(()) + } + + async fn find_existing_issue( + &self, + owner: &str, + repo: &str, + fingerprint: &str, + ) -> Result, CoreError> { + let query = format!("repo:{owner}/{repo} is:issue {fingerprint}"); + let results = self + .client + .search() + .issues_and_pull_requests(&query) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("GitHub search failed: {e}")))?; + + if let Some(issue) = results.items.first() { + Ok(Some(TrackerIssue::new( + String::new(), + TrackerType::GitHub, + issue.number.to_string(), + issue.html_url.to_string(), + issue.title.clone(), + ))) + } else { + Ok(None) + } + } +} diff --git a/compliance-agent/src/trackers/gitlab.rs b/compliance-agent/src/trackers/gitlab.rs new file mode 100644 index 0000000..6791ec2 --- /dev/null +++ b/compliance-agent/src/trackers/gitlab.rs @@ -0,0 +1,201 @@ +use compliance_core::error::CoreError; +use compliance_core::models::{TrackerIssue, TrackerType}; +use compliance_core::traits::issue_tracker::{IssueTracker, ReviewComment}; +use secrecy::{ExposeSecret, SecretString}; + +pub struct GitLabTracker { + base_url: String, + http: reqwest::Client, + token: SecretString, +} + +impl GitLabTracker { + pub fn new(base_url: String, token: SecretString) -> Self { + Self { + base_url: base_url.trim_end_matches('/').to_string(), + http: reqwest::Client::new(), + token, + } + } + + fn api_url(&self, path: &str) -> String { + format!("{}/api/v4{}", self.base_url, path) + } + + fn project_path(owner: &str, repo: &str) -> String { + urlencoding::encode(&format!("{owner}/{repo}")).to_string() + } +} + +impl IssueTracker for GitLabTracker { + fn name(&self) -> &str { + "gitlab" + } + + async fn create_issue( + &self, + owner: &str, + repo: &str, + title: &str, + body: &str, + labels: &[String], + ) -> Result { + let project = Self::project_path(owner, repo); + let url = self.api_url(&format!("/projects/{project}/issues")); + + let mut payload = serde_json::json!({ + "title": title, + "description": body, + }); + if !labels.is_empty() { + payload["labels"] = serde_json::Value::String(labels.join(",")); + } + + let resp = self + .http + .post(&url) + .header("PRIVATE-TOKEN", self.token.expose_secret()) + .json(&payload) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("GitLab create issue failed: {e}")))?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(CoreError::IssueTracker(format!("GitLab returned {status}: {body}"))); + } + + let issue: serde_json::Value = resp.json().await + .map_err(|e| CoreError::IssueTracker(format!("Failed to parse GitLab response: {e}")))?; + + Ok(TrackerIssue::new( + String::new(), + TrackerType::GitLab, + issue["iid"].to_string(), + issue["web_url"].as_str().unwrap_or("").to_string(), + title.to_string(), + )) + } + + async fn update_issue_status( + &self, + owner: &str, + repo: &str, + external_id: &str, + status: &str, + ) -> Result<(), CoreError> { + let project = Self::project_path(owner, repo); + let url = self.api_url(&format!("/projects/{project}/issues/{external_id}")); + + let state_event = match status { + "closed" | "resolved" => "close", + _ => "reopen", + }; + + self.http + .put(&url) + .header("PRIVATE-TOKEN", self.token.expose_secret()) + .json(&serde_json::json!({ "state_event": state_event })) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("GitLab update issue failed: {e}")))?; + + Ok(()) + } + + async fn add_comment( + &self, + owner: &str, + repo: &str, + external_id: &str, + body: &str, + ) -> Result<(), CoreError> { + let project = Self::project_path(owner, repo); + let url = self.api_url(&format!("/projects/{project}/issues/{external_id}/notes")); + + self.http + .post(&url) + .header("PRIVATE-TOKEN", self.token.expose_secret()) + .json(&serde_json::json!({ "body": body })) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("GitLab add comment failed: {e}")))?; + + Ok(()) + } + + async fn create_pr_review( + &self, + owner: &str, + repo: &str, + pr_number: u64, + body: &str, + comments: Vec, + ) -> Result<(), CoreError> { + let project = Self::project_path(owner, repo); + + // Post overall review as MR note + let note_url = self.api_url(&format!("/projects/{project}/merge_requests/{pr_number}/notes")); + self.http + .post(¬e_url) + .header("PRIVATE-TOKEN", self.token.expose_secret()) + .json(&serde_json::json!({ "body": body })) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("GitLab MR note failed: {e}")))?; + + // Post individual line comments as MR discussions + for comment in comments { + let disc_url = self.api_url(&format!("/projects/{project}/merge_requests/{pr_number}/discussions")); + let payload = serde_json::json!({ + "body": comment.body, + "position": { + "position_type": "text", + "new_path": comment.path, + "new_line": comment.line, + } + }); + let _ = self + .http + .post(&disc_url) + .header("PRIVATE-TOKEN", self.token.expose_secret()) + .json(&payload) + .send() + .await; + } + + Ok(()) + } + + async fn find_existing_issue( + &self, + owner: &str, + repo: &str, + fingerprint: &str, + ) -> Result, CoreError> { + let project = Self::project_path(owner, repo); + let url = self.api_url(&format!("/projects/{project}/issues?search={fingerprint}")); + + let resp = self + .http + .get(&url) + .header("PRIVATE-TOKEN", self.token.expose_secret()) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("GitLab search failed: {e}")))?; + + let issues: Vec = resp.json().await.unwrap_or_default(); + if let Some(issue) = issues.first() { + Ok(Some(TrackerIssue::new( + String::new(), + TrackerType::GitLab, + issue["iid"].to_string(), + issue["web_url"].as_str().unwrap_or("").to_string(), + issue["title"].as_str().unwrap_or("").to_string(), + ))) + } else { + Ok(None) + } + } +} diff --git a/compliance-agent/src/trackers/jira.rs b/compliance-agent/src/trackers/jira.rs new file mode 100644 index 0000000..048e238 --- /dev/null +++ b/compliance-agent/src/trackers/jira.rs @@ -0,0 +1,231 @@ +use compliance_core::error::CoreError; +use compliance_core::models::{TrackerIssue, TrackerType}; +use compliance_core::traits::issue_tracker::{IssueTracker, ReviewComment}; +use secrecy::{ExposeSecret, SecretString}; + +pub struct JiraTracker { + base_url: String, + email: String, + api_token: SecretString, + project_key: String, + http: reqwest::Client, +} + +impl JiraTracker { + pub fn new(base_url: String, email: String, api_token: SecretString, project_key: String) -> Self { + Self { + base_url: base_url.trim_end_matches('/').to_string(), + email, + api_token, + project_key, + http: reqwest::Client::new(), + } + } + + fn auth_header(&self) -> String { + use base64::Engine; + let credentials = format!("{}:{}", self.email, self.api_token.expose_secret()); + format!("Basic {}", base64::engine::general_purpose::STANDARD.encode(credentials)) + } +} + +impl IssueTracker for JiraTracker { + fn name(&self) -> &str { + "jira" + } + + async fn create_issue( + &self, + _owner: &str, + _repo: &str, + title: &str, + body: &str, + labels: &[String], + ) -> Result { + let url = format!("{}/rest/api/3/issue", self.base_url); + + let mut payload = serde_json::json!({ + "fields": { + "project": { "key": self.project_key }, + "summary": title, + "description": { + "type": "doc", + "version": 1, + "content": [{ + "type": "paragraph", + "content": [{ + "type": "text", + "text": body, + }] + }] + }, + "issuetype": { "name": "Bug" }, + } + }); + + if !labels.is_empty() { + payload["fields"]["labels"] = serde_json::Value::Array( + labels.iter().map(|l| serde_json::Value::String(l.clone())).collect(), + ); + } + + let resp = self + .http + .post(&url) + .header("Authorization", self.auth_header()) + .header("Content-Type", "application/json") + .json(&payload) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("Jira create issue failed: {e}")))?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(CoreError::IssueTracker(format!("Jira returned {status}: {body}"))); + } + + let issue: serde_json::Value = resp.json().await + .map_err(|e| CoreError::IssueTracker(format!("Failed to parse Jira response: {e}")))?; + + let key = issue["key"].as_str().unwrap_or("").to_string(); + let url = format!("{}/browse/{}", self.base_url, key); + + Ok(TrackerIssue::new( + String::new(), + TrackerType::Jira, + key, + url, + title.to_string(), + )) + } + + async fn update_issue_status( + &self, + _owner: &str, + _repo: &str, + external_id: &str, + status: &str, + ) -> Result<(), CoreError> { + // Get available transitions + let url = format!("{}/rest/api/3/issue/{external_id}/transitions", self.base_url); + let resp = self + .http + .get(&url) + .header("Authorization", self.auth_header()) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("Jira get transitions failed: {e}")))?; + + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + let transitions = body["transitions"].as_array(); + + // Find matching transition + if let Some(transitions) = transitions { + let target = match status { + "closed" | "resolved" => "Done", + "in_progress" => "In Progress", + _ => "To Do", + }; + + if let Some(transition) = transitions.iter().find(|t| { + t["name"].as_str().map(|n| n.eq_ignore_ascii_case(target)).unwrap_or(false) + }) { + let transition_id = transition["id"].as_str().unwrap_or(""); + self.http + .post(&format!("{}/rest/api/3/issue/{external_id}/transitions", self.base_url)) + .header("Authorization", self.auth_header()) + .json(&serde_json::json!({ "transition": { "id": transition_id } })) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("Jira transition failed: {e}")))?; + } + } + + Ok(()) + } + + async fn add_comment( + &self, + _owner: &str, + _repo: &str, + external_id: &str, + body: &str, + ) -> Result<(), CoreError> { + let url = format!("{}/rest/api/3/issue/{external_id}/comment", self.base_url); + + self.http + .post(&url) + .header("Authorization", self.auth_header()) + .header("Content-Type", "application/json") + .json(&serde_json::json!({ + "body": { + "type": "doc", + "version": 1, + "content": [{ + "type": "paragraph", + "content": [{ + "type": "text", + "text": body, + }] + }] + } + })) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("Jira add comment failed: {e}")))?; + + Ok(()) + } + + async fn create_pr_review( + &self, + _owner: &str, + _repo: &str, + _pr_number: u64, + _body: &str, + _comments: Vec, + ) -> Result<(), CoreError> { + // Jira doesn't have native PR reviews - this is a no-op + tracing::info!("Jira doesn't support PR reviews natively, skipping"); + Ok(()) + } + + async fn find_existing_issue( + &self, + _owner: &str, + _repo: &str, + fingerprint: &str, + ) -> Result, CoreError> { + let jql = format!( + "project = {} AND text ~ \"{}\"", + self.project_key, fingerprint + ); + let url = format!("{}/rest/api/3/search", self.base_url); + + let resp = self + .http + .get(&url) + .header("Authorization", self.auth_header()) + .query(&[("jql", &jql), ("maxResults", &"1".to_string())]) + .send() + .await + .map_err(|e| CoreError::IssueTracker(format!("Jira search failed: {e}")))?; + + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + if let Some(issue) = body["issues"].as_array().and_then(|arr| arr.first()) { + let key = issue["key"].as_str().unwrap_or("").to_string(); + let url = format!("{}/browse/{}", self.base_url, key); + let title = issue["fields"]["summary"].as_str().unwrap_or("").to_string(); + Ok(Some(TrackerIssue::new( + String::new(), + TrackerType::Jira, + key, + url, + title, + ))) + } else { + Ok(None) + } + } +} diff --git a/compliance-agent/src/trackers/mod.rs b/compliance-agent/src/trackers/mod.rs new file mode 100644 index 0000000..b4abd57 --- /dev/null +++ b/compliance-agent/src/trackers/mod.rs @@ -0,0 +1,3 @@ +pub mod github; +pub mod gitlab; +pub mod jira; diff --git a/compliance-agent/src/webhooks/github.rs b/compliance-agent/src/webhooks/github.rs new file mode 100644 index 0000000..e94548e --- /dev/null +++ b/compliance-agent/src/webhooks/github.rs @@ -0,0 +1,130 @@ +use std::sync::Arc; + +use axum::body::Bytes; +use axum::extract::Extension; +use axum::http::{HeaderMap, StatusCode}; +use hmac::{Hmac, Mac}; +use secrecy::ExposeSecret; +use sha2::Sha256; + +use compliance_core::models::ScanTrigger; + +use crate::agent::ComplianceAgent; + +type HmacSha256 = Hmac; + +pub async fn handle_github_webhook( + Extension(agent): Extension>, + headers: HeaderMap, + body: Bytes, +) -> StatusCode { + // Verify HMAC signature + if let Some(secret) = &agent.config.github_webhook_secret { + let signature = headers + .get("x-hub-signature-256") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + if !verify_signature(secret.expose_secret(), &body, signature) { + tracing::warn!("GitHub webhook: invalid signature"); + return StatusCode::UNAUTHORIZED; + } + } + + let event = headers + .get("x-github-event") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + let payload: serde_json::Value = match serde_json::from_slice(&body) { + Ok(v) => v, + Err(e) => { + tracing::warn!("GitHub webhook: invalid JSON: {e}"); + return StatusCode::BAD_REQUEST; + } + }; + + match event { + "push" => handle_push(agent, &payload).await, + "pull_request" => handle_pull_request(agent, &payload).await, + _ => { + tracing::debug!("GitHub webhook: ignoring event '{event}'"); + StatusCode::OK + } + } +} + +async fn handle_push(agent: Arc, payload: &serde_json::Value) -> StatusCode { + let repo_url = payload["repository"]["clone_url"] + .as_str() + .or_else(|| payload["repository"]["html_url"].as_str()) + .unwrap_or(""); + + if repo_url.is_empty() { + return StatusCode::BAD_REQUEST; + } + + // Find matching tracked repository + let repo = agent + .db + .repositories() + .find_one(mongodb::bson::doc! { "git_url": repo_url }) + .await + .ok() + .flatten(); + + if let Some(repo) = repo { + let repo_id = repo.id.map(|id| id.to_hex()).unwrap_or_default(); + let agent_clone = (*agent).clone(); + tokio::spawn(async move { + tracing::info!("GitHub push webhook: triggering scan for {repo_id}"); + if let Err(e) = agent_clone.run_scan(&repo_id, ScanTrigger::Webhook).await { + tracing::error!("Webhook-triggered scan failed: {e}"); + } + }); + } else { + tracing::debug!("GitHub push webhook: no tracked repo for {repo_url}"); + } + + StatusCode::OK +} + +async fn handle_pull_request( + _agent: Arc, + payload: &serde_json::Value, +) -> StatusCode { + let action = payload["action"].as_str().unwrap_or(""); + if action != "opened" && action != "synchronize" { + return StatusCode::OK; + } + + let repo_url = payload["repository"]["clone_url"] + .as_str() + .unwrap_or(""); + let pr_number = payload["pull_request"]["number"].as_u64().unwrap_or(0); + + if repo_url.is_empty() || pr_number == 0 { + return StatusCode::BAD_REQUEST; + } + + tracing::info!("GitHub PR webhook: PR #{pr_number} {action} on {repo_url}"); + // PR review scan would be triggered here - runs incremental SAST on diff + // and posts review comments via the GitHub tracker + + StatusCode::OK +} + +fn verify_signature(secret: &str, body: &[u8], signature: &str) -> bool { + let sig = signature.strip_prefix("sha256=").unwrap_or(signature); + let sig_bytes = match hex::decode(sig) { + Ok(b) => b, + Err(_) => return false, + }; + + let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) { + Ok(m) => m, + Err(_) => return false, + }; + mac.update(body); + mac.verify_slice(&sig_bytes).is_ok() +} diff --git a/compliance-agent/src/webhooks/gitlab.rs b/compliance-agent/src/webhooks/gitlab.rs new file mode 100644 index 0000000..8c2d82d --- /dev/null +++ b/compliance-agent/src/webhooks/gitlab.rs @@ -0,0 +1,95 @@ +use std::sync::Arc; + +use axum::body::Bytes; +use axum::extract::Extension; +use axum::http::{HeaderMap, StatusCode}; +use secrecy::ExposeSecret; + +use compliance_core::models::ScanTrigger; + +use crate::agent::ComplianceAgent; + +pub async fn handle_gitlab_webhook( + Extension(agent): Extension>, + headers: HeaderMap, + body: Bytes, +) -> StatusCode { + // Verify GitLab token + if let Some(secret) = &agent.config.gitlab_webhook_secret { + let token = headers + .get("x-gitlab-token") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + if token != secret.expose_secret() { + tracing::warn!("GitLab webhook: invalid token"); + return StatusCode::UNAUTHORIZED; + } + } + + let payload: serde_json::Value = match serde_json::from_slice(&body) { + Ok(v) => v, + Err(e) => { + tracing::warn!("GitLab webhook: invalid JSON: {e}"); + return StatusCode::BAD_REQUEST; + } + }; + + let event_type = payload["object_kind"].as_str().unwrap_or(""); + + match event_type { + "push" => handle_push(agent, &payload).await, + "merge_request" => handle_merge_request(agent, &payload).await, + _ => { + tracing::debug!("GitLab webhook: ignoring event '{event_type}'"); + StatusCode::OK + } + } +} + +async fn handle_push(agent: Arc, payload: &serde_json::Value) -> StatusCode { + let repo_url = payload["project"]["git_http_url"] + .as_str() + .or_else(|| payload["project"]["web_url"].as_str()) + .unwrap_or(""); + + if repo_url.is_empty() { + return StatusCode::BAD_REQUEST; + } + + let repo = agent + .db + .repositories() + .find_one(mongodb::bson::doc! { "git_url": repo_url }) + .await + .ok() + .flatten(); + + if let Some(repo) = repo { + let repo_id = repo.id.map(|id| id.to_hex()).unwrap_or_default(); + let agent_clone = (*agent).clone(); + tokio::spawn(async move { + tracing::info!("GitLab push webhook: triggering scan for {repo_id}"); + if let Err(e) = agent_clone.run_scan(&repo_id, ScanTrigger::Webhook).await { + tracing::error!("Webhook-triggered scan failed: {e}"); + } + }); + } + + StatusCode::OK +} + +async fn handle_merge_request( + _agent: Arc, + payload: &serde_json::Value, +) -> StatusCode { + let action = payload["object_attributes"]["action"].as_str().unwrap_or(""); + if action != "open" && action != "update" { + return StatusCode::OK; + } + + let mr_iid = payload["object_attributes"]["iid"].as_u64().unwrap_or(0); + tracing::info!("GitLab MR webhook: MR !{mr_iid} {action}"); + + StatusCode::OK +} diff --git a/compliance-agent/src/webhooks/mod.rs b/compliance-agent/src/webhooks/mod.rs new file mode 100644 index 0000000..8ca1a34 --- /dev/null +++ b/compliance-agent/src/webhooks/mod.rs @@ -0,0 +1,5 @@ +pub mod github; +pub mod gitlab; +pub mod server; + +pub use server::start_webhook_server; diff --git a/compliance-agent/src/webhooks/server.rs b/compliance-agent/src/webhooks/server.rs new file mode 100644 index 0000000..b695aba --- /dev/null +++ b/compliance-agent/src/webhooks/server.rs @@ -0,0 +1,27 @@ +use std::sync::Arc; + +use axum::routing::post; +use axum::{Extension, Router}; + +use crate::agent::ComplianceAgent; +use crate::error::AgentError; +use crate::webhooks::{github, gitlab}; + +pub async fn start_webhook_server(agent: &ComplianceAgent) -> Result<(), AgentError> { + let app = Router::new() + .route("/webhook/github", post(github::handle_github_webhook)) + .route("/webhook/gitlab", post(gitlab::handle_gitlab_webhook)) + .layer(Extension(Arc::new(agent.clone()))); + + let addr = "0.0.0.0:3002"; + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(|e| AgentError::Other(format!("Failed to bind webhook server: {e}")))?; + + tracing::info!("Webhook server listening on {addr}"); + axum::serve(listener, app) + .await + .map_err(|e| AgentError::Other(format!("Webhook server error: {e}")))?; + + Ok(()) +} diff --git a/compliance-core/Cargo.toml b/compliance-core/Cargo.toml new file mode 100644 index 0000000..5bd151a --- /dev/null +++ b/compliance-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "compliance-core" +version = "0.1.0" +edition = "2021" + +[lints] +workspace = true + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } +thiserror = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +uuid = { workspace = true } +secrecy = { workspace = true } +mongodb = { workspace = true } diff --git a/compliance-core/src/config.rs b/compliance-core/src/config.rs new file mode 100644 index 0000000..1c2ffae --- /dev/null +++ b/compliance-core/src/config.rs @@ -0,0 +1,34 @@ +use secrecy::SecretString; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug)] +pub struct AgentConfig { + pub mongodb_uri: String, + pub mongodb_database: String, + pub litellm_url: String, + pub litellm_api_key: SecretString, + pub litellm_model: String, + pub github_token: Option, + pub github_webhook_secret: Option, + pub gitlab_url: Option, + pub gitlab_token: Option, + pub gitlab_webhook_secret: Option, + pub jira_url: Option, + pub jira_email: Option, + pub jira_api_token: Option, + pub jira_project_key: Option, + pub searxng_url: Option, + pub nvd_api_key: Option, + pub agent_port: u16, + pub scan_schedule: String, + pub cve_monitor_schedule: String, + pub git_clone_base_path: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DashboardConfig { + pub mongodb_uri: String, + pub mongodb_database: String, + pub agent_api_url: String, + pub dashboard_port: u16, +} diff --git a/compliance-core/src/error.rs b/compliance-core/src/error.rs new file mode 100644 index 0000000..a6aba6b --- /dev/null +++ b/compliance-core/src/error.rs @@ -0,0 +1,41 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CoreError { + #[error("Database error: {0}")] + Database(#[from] mongodb::error::Error), + + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("Git error: {0}")] + Git(String), + + #[error("Scanner error: {source}")] + Scanner { + scanner: String, + #[source] + source: Box, + }, + + #[error("LLM error: {0}")] + Llm(String), + + #[error("Issue tracker error: {0}")] + IssueTracker(String), + + #[error("HTTP error: {0}")] + Http(String), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Not found: {0}")] + NotFound(String), + + #[error("{0}")] + Other(String), +} diff --git a/compliance-core/src/lib.rs b/compliance-core/src/lib.rs new file mode 100644 index 0000000..d492d80 --- /dev/null +++ b/compliance-core/src/lib.rs @@ -0,0 +1,7 @@ +pub mod config; +pub mod error; +pub mod models; +pub mod traits; + +pub use config::{AgentConfig, DashboardConfig}; +pub use error::CoreError; diff --git a/compliance-core/src/models/cve.rs b/compliance-core/src/models/cve.rs new file mode 100644 index 0000000..2800613 --- /dev/null +++ b/compliance-core/src/models/cve.rs @@ -0,0 +1,46 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum CveSource { + Osv, + Nvd, + SearXNG, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CveAlert { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub cve_id: String, + pub repo_id: String, + pub affected_package: String, + pub affected_version: String, + pub source: CveSource, + pub severity: Option, + pub cvss_score: Option, + pub summary: Option, + pub llm_impact_summary: Option, + pub references: Vec, + pub created_at: DateTime, +} + +impl CveAlert { + pub fn new(cve_id: String, repo_id: String, affected_package: String, affected_version: String, source: CveSource) -> Self { + Self { + id: None, + cve_id, + repo_id, + affected_package, + affected_version, + source, + severity: None, + cvss_score: None, + summary: None, + llm_impact_summary: None, + references: Vec::new(), + created_at: Utc::now(), + } + } +} diff --git a/compliance-core/src/models/finding.rs b/compliance-core/src/models/finding.rs new file mode 100644 index 0000000..ad8a365 --- /dev/null +++ b/compliance-core/src/models/finding.rs @@ -0,0 +1,115 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::scan::ScanType; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "lowercase")] +pub enum Severity { + Info, + Low, + Medium, + High, + Critical, +} + +impl std::fmt::Display for Severity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Info => write!(f, "info"), + Self::Low => write!(f, "low"), + Self::Medium => write!(f, "medium"), + Self::High => write!(f, "high"), + Self::Critical => write!(f, "critical"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum FindingStatus { + Open, + Triaged, + FalsePositive, + Resolved, + Ignored, +} + +impl std::fmt::Display for FindingStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Open => write!(f, "open"), + Self::Triaged => write!(f, "triaged"), + Self::FalsePositive => write!(f, "false_positive"), + Self::Resolved => write!(f, "resolved"), + Self::Ignored => write!(f, "ignored"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Finding { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub repo_id: String, + pub fingerprint: String, + pub scanner: String, + pub scan_type: ScanType, + pub rule_id: Option, + pub title: String, + pub description: String, + pub severity: Severity, + pub confidence: Option, + pub cwe: Option, + pub cve: Option, + pub cvss_score: Option, + pub file_path: Option, + pub line_number: Option, + pub code_snippet: Option, + pub remediation: Option, + pub suggested_fix: Option, + pub status: FindingStatus, + pub tracker_issue_url: Option, + pub scan_run_id: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Finding { + pub fn new( + repo_id: String, + fingerprint: String, + scanner: String, + scan_type: ScanType, + title: String, + description: String, + severity: Severity, + ) -> Self { + let now = Utc::now(); + Self { + id: None, + repo_id, + fingerprint, + scanner, + scan_type, + rule_id: None, + title, + description, + severity, + confidence: None, + cwe: None, + cve: None, + cvss_score: None, + file_path: None, + line_number: None, + code_snippet: None, + remediation: None, + suggested_fix: None, + status: FindingStatus::Open, + tracker_issue_url: None, + scan_run_id: None, + created_at: now, + updated_at: now, + } + } +} diff --git a/compliance-core/src/models/issue.rs b/compliance-core/src/models/issue.rs new file mode 100644 index 0000000..8b4e694 --- /dev/null +++ b/compliance-core/src/models/issue.rs @@ -0,0 +1,77 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum TrackerType { + GitHub, + GitLab, + Jira, +} + +impl std::fmt::Display for TrackerType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::GitHub => write!(f, "github"), + Self::GitLab => write!(f, "gitlab"), + Self::Jira => write!(f, "jira"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum IssueStatus { + Open, + InProgress, + Closed, + Resolved, +} + +impl std::fmt::Display for IssueStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Open => write!(f, "open"), + Self::InProgress => write!(f, "in_progress"), + Self::Closed => write!(f, "closed"), + Self::Resolved => write!(f, "resolved"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrackerIssue { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub finding_id: String, + pub tracker_type: TrackerType, + pub external_id: String, + pub external_url: String, + pub title: String, + pub status: IssueStatus, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl TrackerIssue { + pub fn new( + finding_id: String, + tracker_type: TrackerType, + external_id: String, + external_url: String, + title: String, + ) -> Self { + let now = Utc::now(); + Self { + id: None, + finding_id, + tracker_type, + external_id, + external_url, + title, + status: IssueStatus::Open, + created_at: now, + updated_at: now, + } + } +} diff --git a/compliance-core/src/models/mod.rs b/compliance-core/src/models/mod.rs new file mode 100644 index 0000000..099b5bc --- /dev/null +++ b/compliance-core/src/models/mod.rs @@ -0,0 +1,13 @@ +pub mod cve; +pub mod finding; +pub mod issue; +pub mod repository; +pub mod sbom; +pub mod scan; + +pub use cve::{CveAlert, CveSource}; +pub use finding::{Finding, FindingStatus, Severity}; +pub use issue::{IssueStatus, TrackerIssue, TrackerType}; +pub use repository::{ScanTrigger, TrackedRepository}; +pub use sbom::{SbomEntry, VulnRef}; +pub use scan::{ScanPhase, ScanRun, ScanRunStatus, ScanType}; diff --git a/compliance-core/src/models/repository.rs b/compliance-core/src/models/repository.rs new file mode 100644 index 0000000..b9d815a --- /dev/null +++ b/compliance-core/src/models/repository.rs @@ -0,0 +1,53 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::issue::TrackerType; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ScanTrigger { + Scheduled, + Webhook, + Manual, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrackedRepository { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub name: String, + pub git_url: String, + pub default_branch: String, + pub local_path: Option, + pub scan_schedule: Option, + pub webhook_enabled: bool, + pub tracker_type: Option, + pub tracker_owner: Option, + pub tracker_repo: Option, + pub last_scanned_commit: Option, + pub findings_count: u32, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl TrackedRepository { + pub fn new(name: String, git_url: String) -> Self { + let now = Utc::now(); + Self { + id: None, + name, + git_url, + default_branch: "main".to_string(), + local_path: None, + scan_schedule: None, + webhook_enabled: false, + tracker_type: None, + tracker_owner: None, + tracker_repo: None, + last_scanned_commit: None, + findings_count: 0, + created_at: now, + updated_at: now, + } + } +} diff --git a/compliance-core/src/models/sbom.rs b/compliance-core/src/models/sbom.rs new file mode 100644 index 0000000..ec6f371 --- /dev/null +++ b/compliance-core/src/models/sbom.rs @@ -0,0 +1,43 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VulnRef { + pub id: String, + pub source: String, + pub severity: Option, + pub url: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SbomEntry { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub repo_id: String, + pub name: String, + pub version: String, + pub package_manager: String, + pub license: Option, + pub purl: Option, + pub known_vulnerabilities: Vec, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl SbomEntry { + pub fn new(repo_id: String, name: String, version: String, package_manager: String) -> Self { + let now = Utc::now(); + Self { + id: None, + repo_id, + name, + version, + package_manager, + license: None, + purl: None, + known_vulnerabilities: Vec::new(), + created_at: now, + updated_at: now, + } + } +} diff --git a/compliance-core/src/models/scan.rs b/compliance-core/src/models/scan.rs new file mode 100644 index 0000000..99ddb74 --- /dev/null +++ b/compliance-core/src/models/scan.rs @@ -0,0 +1,81 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::repository::ScanTrigger; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ScanType { + Sast, + Sbom, + Cve, + Gdpr, + OAuth, +} + +impl std::fmt::Display for ScanType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Sast => write!(f, "sast"), + Self::Sbom => write!(f, "sbom"), + Self::Cve => write!(f, "cve"), + Self::Gdpr => write!(f, "gdpr"), + Self::OAuth => write!(f, "oauth"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ScanRunStatus { + Running, + Completed, + Failed, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ScanPhase { + ChangeDetection, + Sast, + SbomGeneration, + CveScanning, + PatternScanning, + LlmTriage, + IssueCreation, + Completed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScanRun { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option, + pub repo_id: String, + pub trigger: ScanTrigger, + pub commit_sha: Option, + pub status: ScanRunStatus, + pub current_phase: ScanPhase, + pub phases_completed: Vec, + pub new_findings_count: u32, + pub error_message: Option, + pub started_at: DateTime, + pub completed_at: Option>, +} + +impl ScanRun { + pub fn new(repo_id: String, trigger: ScanTrigger) -> Self { + Self { + id: None, + repo_id, + trigger, + commit_sha: None, + status: ScanRunStatus::Running, + current_phase: ScanPhase::ChangeDetection, + phases_completed: Vec::new(), + new_findings_count: 0, + error_message: None, + started_at: Utc::now(), + completed_at: None, + } + } +} diff --git a/compliance-core/src/traits/issue_tracker.rs b/compliance-core/src/traits/issue_tracker.rs new file mode 100644 index 0000000..3c5acc0 --- /dev/null +++ b/compliance-core/src/traits/issue_tracker.rs @@ -0,0 +1,55 @@ +use crate::error::CoreError; +use crate::models::TrackerIssue; + +#[allow(async_fn_in_trait)] +pub trait IssueTracker: Send + Sync { + fn name(&self) -> &str; + + async fn create_issue( + &self, + owner: &str, + repo: &str, + title: &str, + body: &str, + labels: &[String], + ) -> Result; + + async fn update_issue_status( + &self, + owner: &str, + repo: &str, + external_id: &str, + status: &str, + ) -> Result<(), CoreError>; + + async fn add_comment( + &self, + owner: &str, + repo: &str, + external_id: &str, + body: &str, + ) -> Result<(), CoreError>; + + async fn create_pr_review( + &self, + owner: &str, + repo: &str, + pr_number: u64, + body: &str, + comments: Vec, + ) -> Result<(), CoreError>; + + async fn find_existing_issue( + &self, + owner: &str, + repo: &str, + fingerprint: &str, + ) -> Result, CoreError>; +} + +#[derive(Debug, Clone)] +pub struct ReviewComment { + pub path: String, + pub line: u32, + pub body: String, +} diff --git a/compliance-core/src/traits/mod.rs b/compliance-core/src/traits/mod.rs new file mode 100644 index 0000000..e2d1790 --- /dev/null +++ b/compliance-core/src/traits/mod.rs @@ -0,0 +1,5 @@ +pub mod issue_tracker; +pub mod scanner; + +pub use issue_tracker::IssueTracker; +pub use scanner::{ScanOutput, Scanner}; diff --git a/compliance-core/src/traits/scanner.rs b/compliance-core/src/traits/scanner.rs new file mode 100644 index 0000000..5adcf76 --- /dev/null +++ b/compliance-core/src/traits/scanner.rs @@ -0,0 +1,17 @@ +use std::path::Path; + +use crate::error::CoreError; +use crate::models::{Finding, SbomEntry, ScanType}; + +#[derive(Debug, Default)] +pub struct ScanOutput { + pub findings: Vec, + pub sbom_entries: Vec, +} + +#[allow(async_fn_in_trait)] +pub trait Scanner: Send + Sync { + fn name(&self) -> &str; + fn scan_type(&self) -> ScanType; + async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result; +} diff --git a/compliance-dashboard/Cargo.toml b/compliance-dashboard/Cargo.toml new file mode 100644 index 0000000..fcc48d6 --- /dev/null +++ b/compliance-dashboard/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "compliance-dashboard" +version = "0.1.0" +edition = "2021" +default-run = "compliance-dashboard" + +[[bin]] +name = "compliance-dashboard" +path = "../bin/main.rs" + +[lints] +workspace = true + +[features] +web = ["dioxus/web", "dioxus/router", "dioxus/fullstack", "dep:reqwest", "dep:web-sys"] +server = [ + "dioxus/server", + "dioxus/router", + "dioxus/fullstack", + "dep:axum", + "dep:mongodb", + "dep:reqwest", + "dep:tower-http", + "dep:secrecy", + "dep:dotenvy", + "dep:dioxus-cli-config", + "dep:dioxus-fullstack", + "dep:tokio", +] + +[dependencies] +compliance-core = { workspace = true } +dioxus = "=0.7.3" +dioxus-free-icons = { version = "0.10", features = ["bootstrap"] } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } +tracing = { workspace = true } +dioxus-logger = "0.6" +thiserror = { workspace = true } + +# Web-only +reqwest = { workspace = true, optional = true } +web-sys = { version = "0.3", optional = true } + +# Server-only +axum = { version = "0.8", optional = true } +mongodb = { workspace = true, optional = true } +tower-http = { version = "0.6", features = ["cors", "trace"], optional = true } +secrecy = { workspace = true, optional = true } +dotenvy = { version = "0.15", optional = true } +tokio = { workspace = true, optional = true } +dioxus-cli-config = { version = "=0.7.3", optional = true } +dioxus-fullstack = { version = "=0.7.3", optional = true } diff --git a/compliance-dashboard/assets/favicon.svg b/compliance-dashboard/assets/favicon.svg new file mode 100644 index 0000000..0ba70b3 --- /dev/null +++ b/compliance-dashboard/assets/favicon.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compliance-dashboard/assets/main.css b/compliance-dashboard/assets/main.css new file mode 100644 index 0000000..4bbdd35 --- /dev/null +++ b/compliance-dashboard/assets/main.css @@ -0,0 +1,315 @@ +:root { + --sidebar-width: 260px; + --header-height: 56px; + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-card: #1e293b; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --accent: #38bdf8; + --accent-hover: #7dd3fc; + --border: #334155; + --danger: #ef4444; + --warning: #f59e0b; + --success: #22c55e; + --info: #3b82f6; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; +} + +.app-shell { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: var(--sidebar-width); + background: var(--bg-secondary); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 40; + overflow-y: auto; +} + +.sidebar-header { + padding: 20px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 12px; +} + +.sidebar-header h1 { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); +} + +.sidebar-nav { + padding: 12px 8px; + flex: 1; +} + +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + color: var(--text-secondary); + text-decoration: none; + font-size: 14px; + font-weight: 500; + transition: all 0.15s; + cursor: pointer; +} + +.nav-item:hover { + background: rgba(56, 189, 248, 0.1); + color: var(--text-primary); +} + +.nav-item.active { + background: rgba(56, 189, 248, 0.15); + color: var(--accent); +} + +.main-content { + margin-left: var(--sidebar-width); + flex: 1; + padding: 24px 32px; + min-height: 100vh; +} + +.page-header { + margin-bottom: 24px; +} + +.page-header h2 { + font-size: 24px; + font-weight: 700; +} + +.page-header p { + color: var(--text-secondary); + margin-top: 4px; +} + +.stat-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; +} + +.stat-card .label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.stat-card .value { + font-size: 28px; + font-weight: 700; +} + +.card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; + margin-bottom: 16px; +} + +.card-header { + font-size: 16px; + font-weight: 600; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} + +.table-wrapper { + overflow-x: auto; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + text-align: left; + padding: 12px 16px; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); + border-bottom: 1px solid var(--border); + font-weight: 600; +} + +td { + padding: 12px 16px; + border-bottom: 1px solid var(--border); + font-size: 14px; +} + +tr:hover { + background: rgba(56, 189, 248, 0.05); +} + +.badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 9999px; + font-size: 12px; + font-weight: 600; +} + +.badge-critical { background: rgba(239, 68, 68, 0.2); color: #fca5a5; } +.badge-high { background: rgba(249, 115, 22, 0.2); color: #fdba74; } +.badge-medium { background: rgba(245, 158, 11, 0.2); color: #fcd34d; } +.badge-low { background: rgba(34, 197, 94, 0.2); color: #86efac; } +.badge-info { background: rgba(59, 130, 246, 0.2); color: #93c5fd; } + +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + border: none; + cursor: pointer; + transition: all 0.15s; +} + +.btn-primary { + background: var(--accent); + color: #0f172a; +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-ghost { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border); +} + +.btn-ghost:hover { + color: var(--text-primary); + border-color: var(--text-secondary); +} + +.code-block { + background: #0d1117; + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px; + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 13px; + line-height: 1.6; + overflow-x: auto; + white-space: pre; +} + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 16px; +} + +.filter-bar { + display: flex; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.filter-bar select, +.filter-bar input { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px 12px; + color: var(--text-primary); + font-size: 14px; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + font-size: 14px; + font-weight: 500; + margin-bottom: 6px; + color: var(--text-secondary); +} + +.form-group input, +.form-group select { + width: 100%; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 14px; + color: var(--text-primary); + font-size: 14px; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 40px; + color: var(--text-secondary); +} + +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + transition: transform 0.3s; + } + .sidebar.open { + transform: translateX(0); + } + .main-content { + margin-left: 0; + padding: 16px; + } +} diff --git a/compliance-dashboard/assets/tailwind.css b/compliance-dashboard/assets/tailwind.css new file mode 100644 index 0000000..314f56a --- /dev/null +++ b/compliance-dashboard/assets/tailwind.css @@ -0,0 +1 @@ +/* Placeholder - generated by build.rs via bunx @tailwindcss/cli */ diff --git a/compliance-dashboard/src/app.rs b/compliance-dashboard/src/app.rs new file mode 100644 index 0000000..4f5252d --- /dev/null +++ b/compliance-dashboard/src/app.rs @@ -0,0 +1,38 @@ +use dioxus::prelude::*; + +use crate::components::app_shell::AppShell; +use crate::pages::*; + +#[derive(Debug, Clone, Routable, PartialEq)] +#[rustfmt::skip] +pub enum Route { + #[layout(AppShell)] + #[route("/")] + OverviewPage {}, + #[route("/repositories")] + RepositoriesPage {}, + #[route("/findings")] + FindingsPage {}, + #[route("/findings/:id")] + FindingDetailPage { id: String }, + #[route("/sbom")] + SbomPage {}, + #[route("/issues")] + IssuesPage {}, + #[route("/settings")] + SettingsPage {}, +} + +const FAVICON: Asset = asset!("/assets/favicon.svg"); +const MAIN_CSS: Asset = asset!("/assets/main.css"); +const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css"); + +#[component] +pub fn App() -> Element { + rsx! { + document::Link { rel: "icon", href: FAVICON } + document::Link { rel: "stylesheet", href: TAILWIND_CSS } + document::Link { rel: "stylesheet", href: MAIN_CSS } + Router:: {} + } +} diff --git a/compliance-dashboard/src/components/app_shell.rs b/compliance-dashboard/src/components/app_shell.rs new file mode 100644 index 0000000..c982f49 --- /dev/null +++ b/compliance-dashboard/src/components/app_shell.rs @@ -0,0 +1,16 @@ +use dioxus::prelude::*; + +use crate::app::Route; +use crate::components::sidebar::Sidebar; + +#[component] +pub fn AppShell() -> Element { + rsx! { + div { class: "app-shell", + Sidebar {} + main { class: "main-content", + Outlet:: {} + } + } + } +} diff --git a/compliance-dashboard/src/components/code_snippet.rs b/compliance-dashboard/src/components/code_snippet.rs new file mode 100644 index 0000000..b16f115 --- /dev/null +++ b/compliance-dashboard/src/components/code_snippet.rs @@ -0,0 +1,23 @@ +use dioxus::prelude::*; + +#[component] +pub fn CodeSnippet( + code: String, + #[props(default)] file_path: String, + #[props(default)] line_number: u32, +) -> Element { + rsx! { + div { + if !file_path.is_empty() { + div { + style: "font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; font-family: monospace;", + "{file_path}" + if line_number > 0 { + ":{line_number}" + } + } + } + pre { class: "code-block", "{code}" } + } + } +} diff --git a/compliance-dashboard/src/components/mod.rs b/compliance-dashboard/src/components/mod.rs new file mode 100644 index 0000000..aebf095 --- /dev/null +++ b/compliance-dashboard/src/components/mod.rs @@ -0,0 +1,7 @@ +pub mod app_shell; +pub mod code_snippet; +pub mod page_header; +pub mod pagination; +pub mod severity_badge; +pub mod sidebar; +pub mod stat_card; diff --git a/compliance-dashboard/src/components/page_header.rs b/compliance-dashboard/src/components/page_header.rs new file mode 100644 index 0000000..db6b04d --- /dev/null +++ b/compliance-dashboard/src/components/page_header.rs @@ -0,0 +1,13 @@ +use dioxus::prelude::*; + +#[component] +pub fn PageHeader(title: String, #[props(default)] description: String) -> Element { + rsx! { + div { class: "page-header", + h2 { "{title}" } + if !description.is_empty() { + p { "{description}" } + } + } + } +} diff --git a/compliance-dashboard/src/components/pagination.rs b/compliance-dashboard/src/components/pagination.rs new file mode 100644 index 0000000..24c9710 --- /dev/null +++ b/compliance-dashboard/src/components/pagination.rs @@ -0,0 +1,33 @@ +use dioxus::prelude::*; + +#[component] +pub fn Pagination( + current_page: u64, + total_pages: u64, + on_page_change: EventHandler, +) -> Element { + if total_pages <= 1 { + return rsx! {}; + } + + rsx! { + div { class: "pagination", + button { + class: "btn btn-ghost", + disabled: current_page <= 1, + onclick: move |_| on_page_change.call(current_page.saturating_sub(1)), + "Previous" + } + span { + style: "color: var(--text-secondary); font-size: 14px;", + "Page {current_page} of {total_pages}" + } + button { + class: "btn btn-ghost", + disabled: current_page >= total_pages, + onclick: move |_| on_page_change.call(current_page + 1), + "Next" + } + } + } +} diff --git a/compliance-dashboard/src/components/severity_badge.rs b/compliance-dashboard/src/components/severity_badge.rs new file mode 100644 index 0000000..7452f24 --- /dev/null +++ b/compliance-dashboard/src/components/severity_badge.rs @@ -0,0 +1,16 @@ +use dioxus::prelude::*; + +#[component] +pub fn SeverityBadge(severity: String) -> Element { + let class = match severity.to_lowercase().as_str() { + "critical" => "badge badge-critical", + "high" => "badge badge-high", + "medium" => "badge badge-medium", + "low" => "badge badge-low", + _ => "badge badge-info", + }; + + rsx! { + span { class: class, "{severity}" } + } +} diff --git a/compliance-dashboard/src/components/sidebar.rs b/compliance-dashboard/src/components/sidebar.rs new file mode 100644 index 0000000..b183845 --- /dev/null +++ b/compliance-dashboard/src/components/sidebar.rs @@ -0,0 +1,81 @@ +use dioxus::prelude::*; +use dioxus_free_icons::icons::bs_icons::*; +use dioxus_free_icons::Icon; + +use crate::app::Route; + +struct NavItem { + label: &'static str, + route: Route, + icon: Element, +} + +#[component] +pub fn Sidebar() -> Element { + let current_route = use_route::(); + + let nav_items = [ + NavItem { + label: "Overview", + route: Route::OverviewPage {}, + icon: rsx! { Icon { icon: BsSpeedometer2, width: 18, height: 18 } }, + }, + NavItem { + label: "Repositories", + route: Route::RepositoriesPage {}, + icon: rsx! { Icon { icon: BsFolder2Open, width: 18, height: 18 } }, + }, + NavItem { + label: "Findings", + route: Route::FindingsPage {}, + icon: rsx! { Icon { icon: BsShieldExclamation, width: 18, height: 18 } }, + }, + NavItem { + label: "SBOM", + route: Route::SbomPage {}, + icon: rsx! { Icon { icon: BsBoxSeam, width: 18, height: 18 } }, + }, + NavItem { + label: "Issues", + route: Route::IssuesPage {}, + icon: rsx! { Icon { icon: BsListTask, width: 18, height: 18 } }, + }, + NavItem { + label: "Settings", + route: Route::SettingsPage {}, + icon: rsx! { Icon { icon: BsGear, width: 18, height: 18 } }, + }, + ]; + + rsx! { + nav { class: "sidebar", + div { class: "sidebar-header", + Icon { icon: BsShieldCheck, width: 24, height: 24 } + h1 { "Compliance Scanner" } + } + div { class: "sidebar-nav", + for item in nav_items { + { + let is_active = match (¤t_route, &item.route) { + (Route::FindingDetailPage { .. }, Route::FindingsPage {}) => true, + (a, b) => a == b, + }; + let class = if is_active { "nav-item active" } else { "nav-item" }; + rsx! { + Link { + to: item.route.clone(), + class: class, + {item.icon} + span { "{item.label}" } + } + } + } + } + } + div { + style: "padding: 16px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-secondary);", + "v0.1.0" + } + } + } +} diff --git a/compliance-dashboard/src/components/stat_card.rs b/compliance-dashboard/src/components/stat_card.rs new file mode 100644 index 0000000..ee55c26 --- /dev/null +++ b/compliance-dashboard/src/components/stat_card.rs @@ -0,0 +1,21 @@ +use dioxus::prelude::*; + +#[component] +pub fn StatCard( + label: String, + value: String, + #[props(default)] color: String, +) -> Element { + let value_style = if color.is_empty() { + String::new() + } else { + format!("color: {color}") + }; + + rsx! { + div { class: "stat-card", + div { class: "label", "{label}" } + div { class: "value", style: value_style, "{value}" } + } + } +} diff --git a/compliance-dashboard/src/infrastructure/config.rs b/compliance-dashboard/src/infrastructure/config.rs new file mode 100644 index 0000000..953d931 --- /dev/null +++ b/compliance-dashboard/src/infrastructure/config.rs @@ -0,0 +1,18 @@ +use compliance_core::DashboardConfig; + +use super::error::DashboardError; + +pub fn load_config() -> Result { + Ok(DashboardConfig { + mongodb_uri: std::env::var("MONGODB_URI") + .map_err(|_| DashboardError::Config("Missing MONGODB_URI".to_string()))?, + mongodb_database: std::env::var("MONGODB_DATABASE") + .unwrap_or_else(|_| "compliance_scanner".to_string()), + agent_api_url: std::env::var("AGENT_API_URL") + .unwrap_or_else(|_| "http://localhost:3001".to_string()), + dashboard_port: std::env::var("DASHBOARD_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(8080), + }) +} diff --git a/compliance-dashboard/src/infrastructure/database.rs b/compliance-dashboard/src/infrastructure/database.rs new file mode 100644 index 0000000..8bcfa26 --- /dev/null +++ b/compliance-dashboard/src/infrastructure/database.rs @@ -0,0 +1,45 @@ +use mongodb::bson::doc; +use mongodb::{Client, Collection}; + +use compliance_core::models::*; + +use super::error::DashboardError; + +#[derive(Clone, Debug)] +pub struct Database { + inner: mongodb::Database, +} + +impl Database { + pub async fn connect(uri: &str, db_name: &str) -> Result { + let client = Client::with_uri_str(uri).await?; + let db = client.database(db_name); + db.run_command(doc! { "ping": 1 }).await?; + tracing::info!("Dashboard connected to MongoDB '{db_name}'"); + Ok(Self { inner: db }) + } + + pub fn repositories(&self) -> Collection { + self.inner.collection("repositories") + } + + pub fn findings(&self) -> Collection { + self.inner.collection("findings") + } + + pub fn scan_runs(&self) -> Collection { + self.inner.collection("scan_runs") + } + + pub fn sbom_entries(&self) -> Collection { + self.inner.collection("sbom_entries") + } + + pub fn cve_alerts(&self) -> Collection { + self.inner.collection("cve_alerts") + } + + pub fn tracker_issues(&self) -> Collection { + self.inner.collection("tracker_issues") + } +} diff --git a/compliance-dashboard/src/infrastructure/error.rs b/compliance-dashboard/src/infrastructure/error.rs new file mode 100644 index 0000000..8b1c15d --- /dev/null +++ b/compliance-dashboard/src/infrastructure/error.rs @@ -0,0 +1,26 @@ +use dioxus::prelude::*; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DashboardError { + #[error("Database error: {0}")] + Database(#[from] mongodb::error::Error), + + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + + #[error("Configuration error: {0}")] + Config(String), + + #[error("{0}")] + Other(String), +} + +impl From for ServerFnError { + fn from(err: DashboardError) -> Self { + ServerFnError::new(err.to_string()) + } +} diff --git a/compliance-dashboard/src/infrastructure/findings.rs b/compliance-dashboard/src/infrastructure/findings.rs new file mode 100644 index 0000000..2373a1f --- /dev/null +++ b/compliance-dashboard/src/infrastructure/findings.rs @@ -0,0 +1,71 @@ +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +use compliance_core::models::Finding; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct FindingsListResponse { + pub data: Vec, + pub total: Option, + pub page: Option, +} + +#[server] +pub async fn fetch_findings( + page: u64, + severity: String, + scan_type: String, + status: String, + repo_id: String, +) -> Result { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + + let mut url = format!("{}/api/v1/findings?page={page}&limit=20", state.agent_api_url); + if !severity.is_empty() { + url.push_str(&format!("&severity={severity}")); + } + if !scan_type.is_empty() { + url.push_str(&format!("&scan_type={scan_type}")); + } + if !status.is_empty() { + url.push_str(&format!("&status={status}")); + } + if !repo_id.is_empty() { + url.push_str(&format!("&repo_id={repo_id}")); + } + + let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; + let body: FindingsListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + Ok(body) +} + +#[server] +pub async fn fetch_finding_detail(id: String) -> Result { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/findings/{id}", state.agent_api_url); + + let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; + let body: serde_json::Value = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + let finding: Finding = serde_json::from_value(body["data"].clone()) + .map_err(|e| ServerFnError::new(e.to_string()))?; + Ok(finding) +} + +#[server] +pub async fn update_finding_status(id: String, status: String) -> Result<(), ServerFnError> { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/findings/{id}/status", state.agent_api_url); + + let client = reqwest::Client::new(); + client + .patch(&url) + .json(&serde_json::json!({ "status": status })) + .send() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + + Ok(()) +} diff --git a/compliance-dashboard/src/infrastructure/issues.rs b/compliance-dashboard/src/infrastructure/issues.rs new file mode 100644 index 0000000..9195868 --- /dev/null +++ b/compliance-dashboard/src/infrastructure/issues.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +use compliance_core::models::TrackerIssue; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct IssuesListResponse { + pub data: Vec, + pub total: Option, + pub page: Option, +} + +#[server] +pub async fn fetch_issues(page: u64) -> Result { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/issues?page={page}&limit=20", state.agent_api_url); + + let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; + let body: IssuesListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + Ok(body) +} diff --git a/compliance-dashboard/src/infrastructure/mod.rs b/compliance-dashboard/src/infrastructure/mod.rs new file mode 100644 index 0000000..762ff29 --- /dev/null +++ b/compliance-dashboard/src/infrastructure/mod.rs @@ -0,0 +1,13 @@ +pub mod config; +pub mod database; +pub mod error; +pub mod findings; +pub mod issues; +pub mod repositories; +pub mod sbom; +pub mod scans; +pub mod server; +pub mod server_state; +pub mod stats; + +pub use server::server_start; diff --git a/compliance-dashboard/src/infrastructure/repositories.rs b/compliance-dashboard/src/infrastructure/repositories.rs new file mode 100644 index 0000000..a4a1278 --- /dev/null +++ b/compliance-dashboard/src/infrastructure/repositories.rs @@ -0,0 +1,64 @@ +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +use compliance_core::models::TrackedRepository; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct RepositoryListResponse { + pub data: Vec, + pub total: Option, + pub page: Option, +} + +#[server] +pub async fn fetch_repositories(page: u64) -> Result { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/repositories?page={page}&limit=20", state.agent_api_url); + + let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; + let body: RepositoryListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + Ok(body) +} + +#[server] +pub async fn add_repository(name: String, git_url: String, default_branch: String) -> Result<(), ServerFnError> { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/repositories", state.agent_api_url); + + let client = reqwest::Client::new(); + let resp = client + .post(&url) + .json(&serde_json::json!({ + "name": name, + "git_url": git_url, + "default_branch": default_branch, + })) + .send() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + + if !resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + return Err(ServerFnError::new(format!("Failed to add repository: {body}"))); + } + + Ok(()) +} + +#[server] +pub async fn trigger_repo_scan(repo_id: String) -> Result<(), ServerFnError> { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/repositories/{repo_id}/scan", state.agent_api_url); + + let client = reqwest::Client::new(); + client + .post(&url) + .send() + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + + Ok(()) +} diff --git a/compliance-dashboard/src/infrastructure/sbom.rs b/compliance-dashboard/src/infrastructure/sbom.rs new file mode 100644 index 0000000..566037f --- /dev/null +++ b/compliance-dashboard/src/infrastructure/sbom.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +use compliance_core::models::SbomEntry; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct SbomListResponse { + pub data: Vec, + pub total: Option, + pub page: Option, +} + +#[server] +pub async fn fetch_sbom(page: u64) -> Result { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/sbom?page={page}&limit=50", state.agent_api_url); + + let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; + let body: SbomListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + Ok(body) +} diff --git a/compliance-dashboard/src/infrastructure/scans.rs b/compliance-dashboard/src/infrastructure/scans.rs new file mode 100644 index 0000000..5ca51ab --- /dev/null +++ b/compliance-dashboard/src/infrastructure/scans.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +use compliance_core::models::ScanRun; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ScansListResponse { + pub data: Vec, + pub total: Option, + pub page: Option, +} + +#[server] +pub async fn fetch_scan_runs(page: u64) -> Result { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/scan-runs?page={page}&limit=20", state.agent_api_url); + + let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; + let body: ScansListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + Ok(body) +} diff --git a/compliance-dashboard/src/infrastructure/server.rs b/compliance-dashboard/src/infrastructure/server.rs new file mode 100644 index 0000000..0df8a46 --- /dev/null +++ b/compliance-dashboard/src/infrastructure/server.rs @@ -0,0 +1,41 @@ +use dioxus::prelude::*; + +use super::config; +use super::database::Database; +use super::error::DashboardError; +use super::server_state::{ServerState, ServerStateInner}; + +pub fn server_start(app: fn() -> Element) -> Result<(), DashboardError> { + tokio::runtime::Runtime::new() + .map_err(|e| DashboardError::Other(e.to_string()))? + .block_on(async move { + dotenvy::dotenv().ok(); + + let config = config::load_config()?; + let db = Database::connect(&config.mongodb_uri, &config.mongodb_database).await?; + + let server_state: ServerState = ServerStateInner { + agent_api_url: config.agent_api_url.clone(), + db, + config, + } + .into(); + + let addr = dioxus_cli_config::fullstack_address_or_localhost(); + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(|e| DashboardError::Other(format!("Failed to bind: {e}")))?; + + tracing::info!("Dashboard server listening on {addr}"); + + let router = axum::Router::new() + .serve_dioxus_application(ServeConfig::new(), app) + .layer(axum::Extension(server_state)); + + axum::serve(listener, router.into_make_service()) + .await + .map_err(|e| DashboardError::Other(format!("Server error: {e}")))?; + + Ok(()) + }) +} diff --git a/compliance-dashboard/src/infrastructure/server_state.rs b/compliance-dashboard/src/infrastructure/server_state.rs new file mode 100644 index 0000000..9f6cec2 --- /dev/null +++ b/compliance-dashboard/src/infrastructure/server_state.rs @@ -0,0 +1,46 @@ +use std::ops::Deref; +use std::sync::Arc; + +use compliance_core::DashboardConfig; + +use super::database::Database; + +#[derive(Clone)] +pub struct ServerState(Arc); + +impl Deref for ServerState { + type Target = ServerStateInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct ServerStateInner { + pub db: Database, + pub config: DashboardConfig, + pub agent_api_url: String, +} + +impl From for ServerState { + fn from(inner: ServerStateInner) -> Self { + Self(Arc::new(inner)) + } +} + +impl axum::extract::FromRequestParts for ServerState +where + S: Send + Sync, +{ + type Rejection = axum::http::StatusCode; + + async fn from_request_parts( + parts: &mut axum::http::request::Parts, + _state: &S, + ) -> Result { + parts + .extensions + .get::() + .cloned() + .ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR) + } +} diff --git a/compliance-dashboard/src/infrastructure/stats.rs b/compliance-dashboard/src/infrastructure/stats.rs new file mode 100644 index 0000000..7976051 --- /dev/null +++ b/compliance-dashboard/src/infrastructure/stats.rs @@ -0,0 +1,27 @@ +use dioxus::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct OverviewStats { + pub total_repositories: u64, + pub total_findings: u64, + pub critical_findings: u64, + pub high_findings: u64, + pub medium_findings: u64, + pub low_findings: u64, + pub total_sbom_entries: u64, + pub total_cve_alerts: u64, + pub total_issues: u64, +} + +#[server] +pub async fn fetch_overview_stats() -> Result { + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + let url = format!("{}/api/v1/stats/overview", state.agent_api_url); + + let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?; + let body: serde_json::Value = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?; + let stats: OverviewStats = serde_json::from_value(body["data"].clone()).unwrap_or_default(); + Ok(stats) +} diff --git a/compliance-dashboard/src/lib.rs b/compliance-dashboard/src/lib.rs new file mode 100644 index 0000000..2a61e0f --- /dev/null +++ b/compliance-dashboard/src/lib.rs @@ -0,0 +1,8 @@ +pub mod app; +pub mod components; +pub mod pages; + +#[cfg(feature = "server")] +pub mod infrastructure; + +pub use app::App; diff --git a/compliance-dashboard/src/pages/finding_detail.rs b/compliance-dashboard/src/pages/finding_detail.rs new file mode 100644 index 0000000..050a4ff --- /dev/null +++ b/compliance-dashboard/src/pages/finding_detail.rs @@ -0,0 +1,117 @@ +use dioxus::prelude::*; + +use crate::components::code_snippet::CodeSnippet; +use crate::components::page_header::PageHeader; +use crate::components::severity_badge::SeverityBadge; + +#[component] +pub fn FindingDetailPage(id: String) -> Element { + let finding_id = id.clone(); + + let finding = use_resource(move || { + let fid = finding_id.clone(); + async move { + crate::infrastructure::findings::fetch_finding_detail(fid).await.ok() + } + }); + + let snapshot = finding.read().clone(); + + match snapshot { + Some(Some(f)) => { + let finding_id_for_status = id.clone(); + rsx! { + PageHeader { + title: f.title.clone(), + description: format!("{} | {} | {}", f.scanner, f.scan_type, f.status), + } + + div { style: "display: flex; gap: 8px; margin-bottom: 16px;", + SeverityBadge { severity: f.severity.to_string() } + if let Some(cwe) = &f.cwe { + span { class: "badge badge-info", "{cwe}" } + } + if let Some(cve) = &f.cve { + span { class: "badge badge-high", "{cve}" } + } + if let Some(score) = f.cvss_score { + span { class: "badge badge-medium", "CVSS: {score}" } + } + } + + div { class: "card", + div { class: "card-header", "Description" } + p { style: "line-height: 1.6;", "{f.description}" } + } + + if let Some(code) = &f.code_snippet { + div { class: "card", + div { class: "card-header", "Code Evidence" } + CodeSnippet { + code: code.clone(), + file_path: f.file_path.clone().unwrap_or_default(), + line_number: f.line_number.unwrap_or(0), + } + } + } + + if let Some(remediation) = &f.remediation { + div { class: "card", + div { class: "card-header", "Remediation" } + p { style: "line-height: 1.6;", "{remediation}" } + } + } + + if let Some(fix) = &f.suggested_fix { + div { class: "card", + div { class: "card-header", "Suggested Fix" } + CodeSnippet { code: fix.clone() } + } + } + + if let Some(url) = &f.tracker_issue_url { + div { class: "card", + div { class: "card-header", "Linked Issue" } + a { + href: "{url}", + target: "_blank", + style: "color: var(--accent);", + "{url}" + } + } + } + + div { class: "card", + div { class: "card-header", "Update Status" } + div { style: "display: flex; gap: 8px;", + for status in ["open", "triaged", "resolved", "false_positive", "ignored"] { + { + let status_str = status.to_string(); + let id_clone = finding_id_for_status.clone(); + rsx! { + button { + class: "btn btn-ghost", + onclick: move |_| { + let s = status_str.clone(); + let id = id_clone.clone(); + spawn(async move { + let _ = crate::infrastructure::findings::update_finding_status(id, s).await; + }); + }, + "{status}" + } + } + } + } + } + } + } + }, + Some(None) => rsx! { + div { class: "card", p { "Finding not found." } } + }, + None => rsx! { + div { class: "loading", "Loading finding..." } + }, + } +} diff --git a/compliance-dashboard/src/pages/findings.rs b/compliance-dashboard/src/pages/findings.rs new file mode 100644 index 0000000..66f2162 --- /dev/null +++ b/compliance-dashboard/src/pages/findings.rs @@ -0,0 +1,124 @@ +use dioxus::prelude::*; + +use crate::app::Route; +use crate::components::page_header::PageHeader; +use crate::components::pagination::Pagination; +use crate::components::severity_badge::SeverityBadge; + +#[component] +pub fn FindingsPage() -> Element { + let mut page = use_signal(|| 1u64); + let mut severity_filter = use_signal(String::new); + let mut type_filter = use_signal(String::new); + let mut status_filter = use_signal(String::new); + + let findings = use_resource(move || { + let p = page(); + let sev = severity_filter(); + let typ = type_filter(); + let stat = status_filter(); + async move { + crate::infrastructure::findings::fetch_findings(p, sev, typ, stat, String::new()).await.ok() + } + }); + + rsx! { + PageHeader { + title: "Findings", + description: "Security and compliance findings across all repositories", + } + + div { class: "filter-bar", + select { + onchange: move |e| { severity_filter.set(e.value()); page.set(1); }, + option { value: "", "All Severities" } + option { value: "critical", "Critical" } + option { value: "high", "High" } + option { value: "medium", "Medium" } + option { value: "low", "Low" } + option { value: "info", "Info" } + } + select { + onchange: move |e| { type_filter.set(e.value()); page.set(1); }, + option { value: "", "All Types" } + option { value: "sast", "SAST" } + option { value: "sbom", "SBOM" } + option { value: "cve", "CVE" } + option { value: "gdpr", "GDPR" } + option { value: "oauth", "OAuth" } + } + select { + onchange: move |e| { status_filter.set(e.value()); page.set(1); }, + option { value: "", "All Statuses" } + option { value: "open", "Open" } + option { value: "triaged", "Triaged" } + option { value: "resolved", "Resolved" } + option { value: "false_positive", "False Positive" } + option { value: "ignored", "Ignored" } + } + } + + match &*findings.read() { + Some(Some(resp)) => { + let total_pages = resp.total.unwrap_or(0).div_ceil(20).max(1); + rsx! { + div { class: "card", + div { class: "table-wrapper", + table { + thead { + tr { + th { "Severity" } + th { "Title" } + th { "Type" } + th { "Scanner" } + th { "File" } + th { "Status" } + } + } + tbody { + for finding in &resp.data { + { + let id = finding.id.as_ref().map(|id| id.to_hex()).unwrap_or_default(); + rsx! { + tr { + td { SeverityBadge { severity: finding.severity.to_string() } } + td { + Link { + to: Route::FindingDetailPage { id: id }, + style: "color: var(--accent); text-decoration: none;", + "{finding.title}" + } + } + td { "{finding.scan_type}" } + td { "{finding.scanner}" } + td { + style: "font-family: monospace; font-size: 12px;", + "{finding.file_path.as_deref().unwrap_or(\"-\")}" + } + td { + span { class: "badge badge-info", "{finding.status}" } + } + } + } + } + } + } + } + } + Pagination { + current_page: page(), + total_pages: total_pages, + on_page_change: move |p| page.set(p), + } + } + } + }, + Some(None) => rsx! { + div { class: "card", p { "Failed to load findings." } } + }, + None => rsx! { + div { class: "loading", "Loading findings..." } + }, + } + } +} diff --git a/compliance-dashboard/src/pages/issues.rs b/compliance-dashboard/src/pages/issues.rs new file mode 100644 index 0000000..b69de92 --- /dev/null +++ b/compliance-dashboard/src/pages/issues.rs @@ -0,0 +1,87 @@ +use dioxus::prelude::*; + +use crate::components::page_header::PageHeader; +use crate::components::pagination::Pagination; + +#[component] +pub fn IssuesPage() -> Element { + let mut page = use_signal(|| 1u64); + + let issues = use_resource(move || { + let p = page(); + async move { + crate::infrastructure::issues::fetch_issues(p).await.ok() + } + }); + + rsx! { + PageHeader { + title: "Issues", + description: "Cross-tracker issue view - GitHub, GitLab, and Jira", + } + + match &*issues.read() { + Some(Some(resp)) => { + let total_pages = resp.total.unwrap_or(0).div_ceil(20).max(1); + rsx! { + div { class: "card", + div { class: "table-wrapper", + table { + thead { + tr { + th { "Tracker" } + th { "ID" } + th { "Title" } + th { "Status" } + th { "Created" } + th { "Link" } + } + } + tbody { + for issue in &resp.data { + tr { + td { + span { class: "badge badge-info", "{issue.tracker_type}" } + } + td { + style: "font-family: monospace;", + "{issue.external_id}" + } + td { "{issue.title}" } + td { + span { class: "badge badge-info", "{issue.status}" } + } + td { + style: "font-size: 12px; color: var(--text-secondary);", + {issue.created_at.format("%Y-%m-%d %H:%M").to_string()} + } + td { + a { + href: "{issue.external_url}", + target: "_blank", + style: "color: var(--accent); text-decoration: none;", + "Open" + } + } + } + } + } + } + } + Pagination { + current_page: page(), + total_pages: total_pages, + on_page_change: move |p| page.set(p), + } + } + } + }, + Some(None) => rsx! { + div { class: "card", p { "Failed to load issues." } } + }, + None => rsx! { + div { class: "loading", "Loading issues..." } + }, + } + } +} diff --git a/compliance-dashboard/src/pages/mod.rs b/compliance-dashboard/src/pages/mod.rs new file mode 100644 index 0000000..cfed572 --- /dev/null +++ b/compliance-dashboard/src/pages/mod.rs @@ -0,0 +1,15 @@ +pub mod finding_detail; +pub mod findings; +pub mod issues; +pub mod overview; +pub mod repositories; +pub mod sbom; +pub mod settings; + +pub use finding_detail::FindingDetailPage; +pub use findings::FindingsPage; +pub use issues::IssuesPage; +pub use overview::OverviewPage; +pub use repositories::RepositoriesPage; +pub use sbom::SbomPage; +pub use settings::SettingsPage; diff --git a/compliance-dashboard/src/pages/overview.rs b/compliance-dashboard/src/pages/overview.rs new file mode 100644 index 0000000..2c3bda8 --- /dev/null +++ b/compliance-dashboard/src/pages/overview.rs @@ -0,0 +1,104 @@ +use dioxus::prelude::*; + +use crate::components::page_header::PageHeader; +use crate::components::stat_card::StatCard; + +#[cfg(feature = "server")] +use crate::infrastructure::stats::fetch_overview_stats; + +#[component] +pub fn OverviewPage() -> Element { + let stats = use_resource(move || async move { + #[cfg(feature = "server")] + { + fetch_overview_stats().await.ok() + } + #[cfg(not(feature = "server"))] + { + crate::infrastructure::stats::fetch_overview_stats().await.ok() + } + }); + + rsx! { + PageHeader { + title: "Overview", + description: "Security and compliance scanning dashboard", + } + + match &*stats.read() { + Some(Some(s)) => rsx! { + div { class: "stat-cards", + StatCard { label: "Repositories", value: s.total_repositories.to_string() } + StatCard { label: "Total Findings", value: s.total_findings.to_string() } + StatCard { + label: "Critical", + value: s.critical_findings.to_string(), + color: "var(--danger)", + } + StatCard { + label: "High", + value: s.high_findings.to_string(), + color: "#f97316", + } + StatCard { + label: "Medium", + value: s.medium_findings.to_string(), + color: "var(--warning)", + } + StatCard { + label: "Low", + value: s.low_findings.to_string(), + color: "var(--success)", + } + StatCard { label: "Dependencies", value: s.total_sbom_entries.to_string() } + StatCard { label: "CVE Alerts", value: s.total_cve_alerts.to_string() } + StatCard { label: "Tracker Issues", value: s.total_issues.to_string() } + } + + div { class: "card", + div { class: "card-header", "Severity Distribution" } + div { + style: "display: flex; gap: 8px; align-items: flex-end; height: 200px; padding: 16px;", + SeverityBar { label: "Critical", count: s.critical_findings, max: s.total_findings, color: "var(--danger)" } + SeverityBar { label: "High", count: s.high_findings, max: s.total_findings, color: "#f97316" } + SeverityBar { label: "Medium", count: s.medium_findings, max: s.total_findings, color: "var(--warning)" } + SeverityBar { label: "Low", count: s.low_findings, max: s.total_findings, color: "var(--success)" } + } + } + }, + Some(None) => rsx! { + div { class: "card", + p { style: "color: var(--text-secondary);", + "Unable to load stats. Make sure the agent API is running." + } + } + }, + None => rsx! { + div { class: "loading", "Loading overview..." } + }, + } + } +} + +#[component] +fn SeverityBar(label: String, count: u64, max: u64, color: String) -> Element { + let height_pct = if max > 0 { (count as f64 / max as f64) * 100.0 } else { 0.0 }; + let height = format!("{}%", height_pct.max(2.0)); + + rsx! { + div { + style: "flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px;", + div { + style: "font-size: 14px; font-weight: 600;", + "{count}" + } + div { + style: "width: 100%; background: {color}; border-radius: 4px 4px 0 0; height: {height}; min-height: 4px; transition: height 0.3s;", + } + div { + style: "font-size: 11px; color: var(--text-secondary);", + "{label}" + } + } + } +} diff --git a/compliance-dashboard/src/pages/repositories.rs b/compliance-dashboard/src/pages/repositories.rs new file mode 100644 index 0000000..e502b30 --- /dev/null +++ b/compliance-dashboard/src/pages/repositories.rs @@ -0,0 +1,155 @@ +use dioxus::prelude::*; + +use crate::components::page_header::PageHeader; +use crate::components::pagination::Pagination; + +#[component] +pub fn RepositoriesPage() -> Element { + let mut page = use_signal(|| 1u64); + let mut show_add_form = use_signal(|| false); + let mut name = use_signal(String::new); + let mut git_url = use_signal(String::new); + let mut branch = use_signal(|| "main".to_string()); + + let repos = use_resource(move || { + let p = page(); + async move { + crate::infrastructure::repositories::fetch_repositories(p).await.ok() + } + }); + + rsx! { + PageHeader { + title: "Repositories", + description: "Tracked git repositories", + } + + div { style: "margin-bottom: 16px;", + button { + class: "btn btn-primary", + onclick: move |_| show_add_form.toggle(), + if show_add_form() { "Cancel" } else { "+ Add Repository" } + } + } + + if show_add_form() { + div { class: "card", + div { class: "card-header", "Add Repository" } + div { class: "form-group", + label { "Name" } + input { + r#type: "text", + placeholder: "my-project", + value: "{name}", + oninput: move |e| name.set(e.value()), + } + } + div { class: "form-group", + label { "Git URL" } + input { + r#type: "text", + placeholder: "https://github.com/org/repo.git", + value: "{git_url}", + oninput: move |e| git_url.set(e.value()), + } + } + div { class: "form-group", + label { "Default Branch" } + input { + r#type: "text", + placeholder: "main", + value: "{branch}", + oninput: move |e| branch.set(e.value()), + } + } + button { + class: "btn btn-primary", + onclick: move |_| { + let n = name(); + let u = git_url(); + let b = branch(); + spawn(async move { + let _ = crate::infrastructure::repositories::add_repository(n, u, b).await; + }); + show_add_form.set(false); + name.set(String::new()); + git_url.set(String::new()); + }, + "Add" + } + } + } + + match &*repos.read() { + Some(Some(resp)) => { + let total_pages = resp.total.unwrap_or(0).div_ceil(20).max(1); + rsx! { + div { class: "card", + div { class: "table-wrapper", + table { + thead { + tr { + th { "Name" } + th { "Git URL" } + th { "Branch" } + th { "Findings" } + th { "Last Scanned" } + th { "Actions" } + } + } + tbody { + for repo in &resp.data { + { + let repo_id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default(); + let repo_id_clone = repo_id.clone(); + rsx! { + tr { + td { "{repo.name}" } + td { + style: "font-size: 12px; font-family: monospace;", + "{repo.git_url}" + } + td { "{repo.default_branch}" } + td { "{repo.findings_count}" } + td { + match &repo.last_scanned_commit { + Some(sha) => rsx! { span { style: "font-family: monospace; font-size: 12px;", "{&sha[..7.min(sha.len())]}" } }, + None => rsx! { span { style: "color: var(--text-secondary);", "Never" } }, + } + } + td { + button { + class: "btn btn-ghost", + onclick: move |_| { + let id = repo_id_clone.clone(); + spawn(async move { + let _ = crate::infrastructure::repositories::trigger_repo_scan(id).await; + }); + }, + "Scan" + } + } + } + } + } + } + } + } + } + Pagination { + current_page: page(), + total_pages: total_pages, + on_page_change: move |p| page.set(p), + } + } + } + }, + Some(None) => rsx! { + div { class: "card", p { "Failed to load repositories." } } + }, + None => rsx! { + div { class: "loading", "Loading repositories..." } + }, + } + } +} diff --git a/compliance-dashboard/src/pages/sbom.rs b/compliance-dashboard/src/pages/sbom.rs new file mode 100644 index 0000000..9bd085a --- /dev/null +++ b/compliance-dashboard/src/pages/sbom.rs @@ -0,0 +1,85 @@ +use dioxus::prelude::*; + +use crate::components::page_header::PageHeader; +use crate::components::pagination::Pagination; + +#[component] +pub fn SbomPage() -> Element { + let mut page = use_signal(|| 1u64); + + let sbom = use_resource(move || { + let p = page(); + async move { + crate::infrastructure::sbom::fetch_sbom(p).await.ok() + } + }); + + rsx! { + PageHeader { + title: "SBOM", + description: "Software Bill of Materials - dependency inventory across all repositories", + } + + match &*sbom.read() { + Some(Some(resp)) => { + let total_pages = resp.total.unwrap_or(0).div_ceil(50).max(1); + rsx! { + div { class: "card", + div { class: "table-wrapper", + table { + thead { + tr { + th { "Package" } + th { "Version" } + th { "Manager" } + th { "License" } + th { "Vulnerabilities" } + } + } + tbody { + for entry in &resp.data { + tr { + td { + style: "font-weight: 500;", + "{entry.name}" + } + td { + style: "font-family: monospace; font-size: 13px;", + "{entry.version}" + } + td { "{entry.package_manager}" } + td { "{entry.license.as_deref().unwrap_or(\"-\")}" } + td { + if entry.known_vulnerabilities.is_empty() { + span { + style: "color: var(--success);", + "None" + } + } else { + span { class: "badge badge-high", + "{entry.known_vulnerabilities.len()} vuln(s)" + } + } + } + } + } + } + } + } + Pagination { + current_page: page(), + total_pages: total_pages, + on_page_change: move |p| page.set(p), + } + } + } + }, + Some(None) => rsx! { + div { class: "card", p { "Failed to load SBOM." } } + }, + None => rsx! { + div { class: "loading", "Loading SBOM..." } + }, + } + } +} diff --git a/compliance-dashboard/src/pages/settings.rs b/compliance-dashboard/src/pages/settings.rs new file mode 100644 index 0000000..0f41fee --- /dev/null +++ b/compliance-dashboard/src/pages/settings.rs @@ -0,0 +1,142 @@ +use dioxus::prelude::*; + +use crate::components::page_header::PageHeader; + +#[component] +pub fn SettingsPage() -> Element { + let mut litellm_url = use_signal(|| "http://localhost:4000".to_string()); + let mut litellm_model = use_signal(|| "gpt-4o".to_string()); + let mut github_token = use_signal(String::new); + let mut gitlab_url = use_signal(|| "https://gitlab.com".to_string()); + let mut gitlab_token = use_signal(String::new); + let mut jira_url = use_signal(String::new); + let mut jira_email = use_signal(String::new); + let mut jira_token = use_signal(String::new); + let mut jira_project = use_signal(String::new); + let mut searxng_url = use_signal(|| "http://localhost:8888".to_string()); + + rsx! { + PageHeader { + title: "Settings", + description: "Configure integrations and scanning parameters", + } + + div { class: "card", + div { class: "card-header", "LiteLLM Configuration" } + div { class: "form-group", + label { "LiteLLM URL" } + input { + r#type: "text", + value: "{litellm_url}", + oninput: move |e| litellm_url.set(e.value()), + } + } + div { class: "form-group", + label { "Model" } + input { + r#type: "text", + value: "{litellm_model}", + oninput: move |e| litellm_model.set(e.value()), + } + } + } + + div { class: "card", + div { class: "card-header", "GitHub Integration" } + div { class: "form-group", + label { "Personal Access Token" } + input { + r#type: "password", + placeholder: "ghp_...", + value: "{github_token}", + oninput: move |e| github_token.set(e.value()), + } + } + } + + div { class: "card", + div { class: "card-header", "GitLab Integration" } + div { class: "form-group", + label { "GitLab URL" } + input { + r#type: "text", + value: "{gitlab_url}", + oninput: move |e| gitlab_url.set(e.value()), + } + } + div { class: "form-group", + label { "Access Token" } + input { + r#type: "password", + placeholder: "glpat-...", + value: "{gitlab_token}", + oninput: move |e| gitlab_token.set(e.value()), + } + } + } + + div { class: "card", + div { class: "card-header", "Jira Integration" } + div { class: "form-group", + label { "Jira URL" } + input { + r#type: "text", + placeholder: "https://your-org.atlassian.net", + value: "{jira_url}", + oninput: move |e| jira_url.set(e.value()), + } + } + div { class: "form-group", + label { "Email" } + input { + r#type: "email", + value: "{jira_email}", + oninput: move |e| jira_email.set(e.value()), + } + } + div { class: "form-group", + label { "API Token" } + input { + r#type: "password", + value: "{jira_token}", + oninput: move |e| jira_token.set(e.value()), + } + } + div { class: "form-group", + label { "Project Key" } + input { + r#type: "text", + placeholder: "SEC", + value: "{jira_project}", + oninput: move |e| jira_project.set(e.value()), + } + } + } + + div { class: "card", + div { class: "card-header", "SearXNG" } + div { class: "form-group", + label { "SearXNG URL" } + input { + r#type: "text", + value: "{searxng_url}", + oninput: move |e| searxng_url.set(e.value()), + } + } + } + + div { style: "margin-top: 16px;", + button { + class: "btn btn-primary", + onclick: move |_| { + tracing::info!("Settings save not yet implemented - settings are managed via .env"); + }, + "Save Settings" + } + p { + style: "margin-top: 8px; font-size: 12px; color: var(--text-secondary);", + "Note: Settings are currently configured via environment variables (.env file). Dashboard-based settings persistence coming soon." + } + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b439a06 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +services: + mongo: + image: mongo:latest + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: example + volumes: + - mongo_data:/data/db + + searxng: + image: searxng/searxng:latest + ports: + - "8888:8080" + environment: + - SEARXNG_BASE_URL=http://localhost:8888 + + agent: + build: + context: . + dockerfile: Dockerfile.agent + ports: + - "3001:3001" + - "3002:3002" + env_file: .env + depends_on: + - mongo + volumes: + - repos_data:/tmp/compliance-scanner/repos + + dashboard: + build: + context: . + dockerfile: Dockerfile.dashboard + ports: + - "8080:8080" + env_file: .env + depends_on: + - mongo + - agent + +volumes: + mongo_data: + repos_data: diff --git a/styles/input.css b/styles/input.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/styles/input.css @@ -0,0 +1 @@ +@import "tailwindcss";