From fd5c788569165c1458ced406dea55a871bcb8b9b Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Fri, 12 Jun 2026 11:25:57 +0300 Subject: [PATCH] ci: add pilot v1 hardening guards --- .github/workflows/ci.yml | 6 +++ .github/workflows/rust-workspace.yml | 31 +++++++++++ .gitignore | 8 ++- private-config/.gitignore | 4 -- private-config/.gitkeep | 1 + private-config/README.md | 13 +++++ scripts/check_portal_contract_sync.mjs | 72 ++++++++++++++++++++++++++ scripts/check_private_config_guard.sh | 25 +++++++++ scripts/quality-gate.sh | 11 ++++ 9 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/rust-workspace.yml delete mode 100755 private-config/.gitignore create mode 100644 private-config/.gitkeep create mode 100644 private-config/README.md create mode 100755 scripts/check_portal_contract_sync.mjs create mode 100755 scripts/check_private_config_guard.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6deb884..71da693 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,12 @@ jobs: - name: Run production inventory placeholder guard self-test run: bash scripts/check_production_inventory_placeholders.sh --self-test + - name: Run private-config guard + run: bash scripts/check_private_config_guard.sh + + - name: Run portal contract sync guard + run: node scripts/check_portal_contract_sync.mjs + rust-runtime-guard: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/rust-workspace.yml b/.github/workflows/rust-workspace.yml new file mode 100644 index 0000000..2660c71 --- /dev/null +++ b/.github/workflows/rust-workspace.yml @@ -0,0 +1,31 @@ +name: rust-workspace + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + rust-workspace: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust 1.85 + uses: dtolnay/rust-toolchain@1.85.0 + with: + components: rustfmt, clippy + + - name: Format + run: cargo fmt --manifest-path adk-rust/Cargo.toml --all -- --check + + - name: Test + run: cargo test --manifest-path adk-rust/Cargo.toml --workspace + + - name: Clippy + run: cargo clippy --manifest-path adk-rust/Cargo.toml --workspace --all-targets -- -D warnings + + - name: Release build + run: cargo build --manifest-path adk-rust/Cargo.toml --workspace --release diff --git a/.gitignore b/.gitignore index ff885e1..1c4ed2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ # Local secrets /secrets/ -/private-config/*.env -/private-config/*.local +/private-config/* +!/private-config/ +!/private-config/README.md +!/private-config/.gitkeep +!/private-config/*.example +!/private-config/*.template /ansible/inventory.ini /codex_history.txt diff --git a/private-config/.gitignore b/private-config/.gitignore deleted file mode 100755 index fac6072..0000000 --- a/private-config/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.env -*.local -!.gitignore -!*.example diff --git a/private-config/.gitkeep b/private-config/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/private-config/.gitkeep @@ -0,0 +1 @@ + diff --git a/private-config/README.md b/private-config/README.md new file mode 100644 index 0000000..d6bd0d5 --- /dev/null +++ b/private-config/README.md @@ -0,0 +1,13 @@ +# private-config + +This directory is reserved for local, host-specific, or secret configuration. + +Do not commit real runtime values here. Git only allows: + +- `private-config/README.md` +- `private-config/.gitkeep` +- `private-config/*.example` +- `private-config/*.template` + +Use `scripts/check_private_config_guard.sh` before commits and in CI to verify +that no private config file has entered the git index. diff --git a/scripts/check_portal_contract_sync.mjs b/scripts/check_portal_contract_sync.mjs new file mode 100755 index 0000000..7035ca9 --- /dev/null +++ b/scripts/check_portal_contract_sync.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, ".."); +const contractPath = path.join( + root, + "adk-rust/crates/detmir-portal/src/contracts/openapi.json", +); + +const requiredPublicPaths = [ + "/api/contracts", + "/api/contracts/openapi.json", + "/api/contracts/typescript.d.ts", + "/api/reports", + "/api/executive", + "/api/workforce", + "/api/security", + "/api/forensics", + "/api/ueba", + "/api/pfsense", + "/api/incidents", + "/api/cases", + "/api/readiness/latest", + "/api/readiness/bundle", + "/api/readiness/verify", +]; + +function fail(message, details = []) { + console.error(message); + for (const detail of details) { + console.error(`- ${detail}`); + } + process.exit(1); +} + +let contract; +try { + contract = JSON.parse(fs.readFileSync(contractPath, "utf8")); +} catch (error) { + fail(`failed to read OpenAPI contract: ${contractPath}`, [error.message]); +} + +if (!contract || typeof contract !== "object" || !contract.paths || typeof contract.paths !== "object") { + fail("OpenAPI contract has no object 'paths' section."); +} + +const contractPaths = Object.keys(contract.paths); +const forbiddenPaths = contractPaths.filter((contractPathName) => + /dioxus|prototype-mirror|mirror/i.test(contractPathName), +); +if (forbiddenPaths.length > 0) { + fail("OpenAPI contract contains legacy/prototype paths.", forbiddenPaths); +} + +const effectivePublicPaths = new Set(); +for (const contractPathName of contractPaths) { + effectivePublicPaths.add(contractPathName); + if (contractPathName.startsWith("/") && !contractPathName.startsWith("/api/")) { + effectivePublicPaths.add(`/api${contractPathName}`); + } +} + +const missingPaths = requiredPublicPaths.filter((requiredPath) => !effectivePublicPaths.has(requiredPath)); +if (missingPaths.length > 0) { + fail("OpenAPI contract is missing required public API paths.", missingPaths); +} + +console.log("portal contract sync guard: OK"); diff --git a/scripts/check_private_config_guard.sh b/scripts/check_private_config_guard.sh new file mode 100755 index 0000000..149ceb2 --- /dev/null +++ b/scripts/check_private_config_guard.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +violations=() + +while IFS= read -r -d '' path; do + rest="${path#private-config/}" + case "$path" in + private-config/README.md|private-config/.gitkeep) + continue + ;; + esac + if [[ "$rest" != */* && ( "$rest" == *.example || "$rest" == *.template ) ]]; then + continue + fi + violations+=("$path") +done < <(git ls-files -z -- private-config) + +if (( ${#violations[@]} > 0 )); then + printf 'private-config guard failed: tracked private files are forbidden. Allowed files are README.md, .gitkeep, *.example, *.template.\\n' >&2 + printf '%s\\n' "${violations[@]}" >&2 + exit 1 +fi + +echo "private-config guard: OK" diff --git a/scripts/quality-gate.sh b/scripts/quality-gate.sh index 2d621bb..2445e54 100755 --- a/scripts/quality-gate.sh +++ b/scripts/quality-gate.sh @@ -7,6 +7,16 @@ cd "$ROOT_DIR" TARGET_ROOT="${CARGO_TARGET_DIR:-$ROOT_DIR/adk-rust/target}" RUST_BIN="${QUALITY_GATE_RUST:-}" +echo "[preflight] Private-config guard" +bash scripts/check_private_config_guard.sh + +echo "[preflight] Portal contract sync guard" +if command -v node >/dev/null 2>&1; then + node scripts/check_portal_contract_sync.mjs +else + echo "node not found, skipping portal contract sync guard." +fi + rust_candidates=() if [[ -n "$RUST_BIN" ]]; then rust_candidates+=("$RUST_BIN") @@ -39,6 +49,7 @@ fi echo "[3/6] Node syntax check (if node available)" if command -v node >/dev/null 2>&1; then node --check scripts/aw-webui-browser-smoke.mjs >/dev/null + node --check scripts/check_portal_contract_sync.mjs >/dev/null else echo "node not found, skipping." fi