diff --git a/.dockerignore b/.dockerignore index e64e9b5..fc01fc8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,6 +15,14 @@ configs frontend/node_modules frontend/dist gensui +!gensui/ +!gensui/** +gensui/.env +gensui/certs +gensui/data +gensui/logs +gensui/frontend/node_modules +gensui/frontend/dist tests certs *.db diff --git a/.env.example b/.env.example index 3bc5e37..0780dde 100644 --- a/.env.example +++ b/.env.example @@ -22,6 +22,16 @@ QDRANT_PATH=./data/qdrant # Security SECRET_KEY=change-me-to-a-random-64-char-string VAULT_ENCRYPTION_KEY=change-me-to-a-fernet-base64-key +SHOGUN_INFRASTRUCTURE_ADMIN_TOKEN= + +# Outbound A2A/Gensui destination policy +A2A_DESTINATION_POLICY=private_allowed +GENSUI_DESTINATION_POLICY=loopback_allowed +OUTBOUND_ALLOWLIST= +ALLOW_HTTP_ON_PRIVATE_NETWORK=true +ALLOW_HTTP_ON_PUBLIC_NETWORK=false +A2A_ALLOWED_PORTS= +GENSUI_ALLOWED_PORTS= # Storage VAULT_PATH=./vault diff --git a/.env.server.example b/.env.server.example index d7df310..c491935 100644 --- a/.env.server.example +++ b/.env.server.example @@ -10,6 +10,16 @@ POSTGRES_PASSWORD=change-me-postgres-password SECRET_KEY=change-me-to-a-random-64-char-string VAULT_ENCRYPTION_KEY=change-me-to-an-independent-random-64-char-string +SHOGUN_INFRASTRUCTURE_ADMIN_TOKEN=change-me-to-an-independent-infrastructure-admin-token + +# Outbound federation policy. Metadata and link-local addresses are always blocked. +A2A_DESTINATION_POLICY=private_allowed +GENSUI_DESTINATION_POLICY=private_allowed +OUTBOUND_ALLOWLIST= +ALLOW_HTTP_ON_PRIVATE_NETWORK=true +ALLOW_HTTP_ON_PUBLIC_NETWORK=false +A2A_ALLOWED_PORTS= +GENSUI_ALLOWED_PORTS= # Optional communication channels. They can also be configured in The Tenshu. TELEGRAM_BOT_TOKEN= diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..103bb65 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: /frontend + schedule: + interval: weekly + - package-ecosystem: npm + directory: /gensui/frontend + schedule: + interval: weekly + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..68f3ce1 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,27 @@ +name: CodeQL + +on: + pull_request: + push: + branches: [main] + schedule: + - cron: "41 3 1 * *" + workflow_dispatch: + +permissions: + contents: read + security-events: write + +jobs: + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: [python, javascript-typescript] + steps: + - uses: actions/checkout@v6 + - uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + - uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/security-hardening.yml b/.github/workflows/security-hardening.yml new file mode 100644 index 0000000..36ac29c --- /dev/null +++ b/.github/workflows/security-hardening.yml @@ -0,0 +1,194 @@ +name: Security hardening + +on: + pull_request: + push: + branches: [main] + schedule: + - cron: "23 4 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + cache: pip + - name: Install CPU runtime and development dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --index-url https://download.pytorch.org/whl/cpu "torch>=2.4.0" + python -m pip install -e ".[dev,office]" + - name: Import and lint + run: | + python -c "import shogun.app; import gensui.app" + # Full-tree lint has substantial pre-existing debt. Gate the security + # boundary and deployment-path files introduced by this remediation. + python -m ruff check \ + shogun/api/a2a.py \ + shogun/api/gensui_config.py \ + shogun/api/infrastructure_auth.py \ + shogun/config.py \ + shogun/integrations/a2a_client.py \ + shogun/services/ssrf_guard.py \ + gensui/app.py \ + gensui/config.py \ + tests/test_ssrf_guard.py \ + tests/test_gensui_frontend.py + - name: Security regression tests + # The repository-wide suite has unrelated pre-existing failures. Keep + # this required gate scoped to the remediation's 39 regression cases. + run: python -m pytest -q tests/test_ssrf_guard.py tests/test_gensui_frontend.py + + frontends: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: Tenshu + directory: frontend + lockfile: frontend/package-lock.json + - name: Gensui + directory: gensui/frontend + lockfile: gensui/frontend/package-lock.json + name: Frontend - ${{ matrix.name }} + defaults: + run: + working-directory: ${{ matrix.directory }} + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: npm + cache-dependency-path: ${{ matrix.lockfile }} + package-manager-cache: true + - run: npm ci + - name: High/Critical dependency gate + run: npm run audit:security + # The repository has pre-existing full-tree lint debt. Keep the complete + # lint command available while gating newly added security boundary code. + - run: npm run lint:security + - run: npm run build + - name: XSS regression tests + if: matrix.directory == 'frontend' + run: npm run test:security + + repository-security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Trivy repository, secret, and misconfiguration scan + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + with: + scan-type: fs + scan-ref: . + scanners: vuln,secret,misconfig + severity: HIGH,CRITICAL + ignore-unfixed: true + trivyignores: .trivyignore.yaml + exit-code: "1" + + gensui-container: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + - name: Build Gensui + run: docker build --file gensui/Dockerfile --tag shogun-gensui:ci . + - name: Scan Gensui image + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + with: + image-ref: shogun-gensui:ci + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: "1" + - name: Run hardened Gensui smoke test + run: | + docker volume create gensui-ci-data + docker volume create gensui-ci-logs + docker run -d --name gensui-ci \ + --read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m \ + --cap-drop ALL --security-opt no-new-privileges:true \ + -e GENSUI_JWT_SECRET=ci-only-random-jwt-secret-0123456789 \ + -e GENSUI_ADMIN_PASSWORD=ci-only-random-admin-password \ + -p 127.0.0.1:8787:8787 \ + -v gensui-ci-data:/app/data -v gensui-ci-logs:/app/logs \ + shogun-gensui:ci + for attempt in $(seq 1 60); do + curl --fail --silent http://127.0.0.1:8787/api/gensui/health && break + sleep 2 + done + curl --fail --silent http://127.0.0.1:8787/ + curl --fail --silent http://127.0.0.1:8787/agents + login_json="$(curl --fail --silent \ + -H 'Content-Type: application/json' \ + -d '{"email":"admin@gensui.local","password":"ci-only-random-admin-password"}' \ + http://127.0.0.1:8787/api/gensui/auth/login)" + admin_token="$(python -c 'import json,sys; print(json.load(sys.stdin)["token"])' <<<"$login_json")" + curl --fail --silent \ + -H "Authorization: Bearer $admin_token" \ + http://127.0.0.1:8787/api/gensui/dashboard + test "$(docker exec gensui-ci whoami)" = "gensui" + docker exec gensui-ci sh -c "grep -R '/api/gensui' /app/frontend/dist/assets >/dev/null" + if docker exec gensui-ci sh -c "grep -R '/api/v1' /app/frontend/dist/assets >/dev/null"; then + echo "Gensui bundle unexpectedly references the Tenshu API prefix" >&2 + exit 1 + fi + - name: Gensui logs + if: failure() + run: docker logs gensui-ci || true + - name: Clean up Gensui + if: always() + run: | + docker rm -f gensui-ci || true + docker volume rm gensui-ci-data gensui-ci-logs || true + + shogun-server-container: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + - name: Build Shogun Server + run: docker build --tag shogun-server:ci . + - name: Prepare Shogun Server environment + run: | + cp .env.server.example .env.server + sed -i 's/change-me-postgres-password/ci-postgres-password/' .env.server + sed -i 's/change-me-to-a-random-64-char-string/ci-application-secret-0123456789/' .env.server + sed -i 's/change-me-to-an-independent-random-64-char-string/ci-vault-secret-0123456789/' .env.server + sed -i 's/change-me-to-an-independent-infrastructure-admin-token/ci-infrastructure-token-0123456789/' .env.server + - name: Scan Shogun image + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 + with: + image-ref: shogun-server:ci + severity: HIGH,CRITICAL + ignore-unfixed: true + exit-code: "1" + - name: Start Shogun Server profile + run: | + docker tag shogun-server:ci shogun-server:local + docker compose --env-file .env.server -f docker-compose.server.yml up -d --no-build + for attempt in $(seq 1 120); do + curl --fail --silent http://127.0.0.1:8000/api/v1/health && break + sleep 2 + done + curl --fail --silent http://127.0.0.1:8000/ + curl --fail --silent http://127.0.0.1:8000/setup + test "$(docker exec shogun-server whoami)" = "shogun" + - name: Launch Mado Chromium as the runtime user + run: | + docker exec shogun-server python -c "import asyncio; from playwright.async_api import async_playwright; exec('async def smoke():\n async with async_playwright() as p:\n browser = await p.chromium.launch(headless=True)\n page = await browser.new_page()\n await page.set_content(\"
$1`)}}),(0,b.jsx)(`div`,{className:`flex-1 h-px bg-shogun-border`})]},e.id):(0,b.jsxs)(`div`,{className:g(`flex gap-3`,n?`flex-row-reverse`:`flex-row`),children:[(0,b.jsx)(`div`,{className:g(`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 text-xs font-bold`,n?`bg-indigo-500/20 text-indigo-400`:`bg-shogun-card border border-shogun-border text-shogun-subdued`),children:(e.from_name||`?`).charAt(0).toUpperCase()}),(0,b.jsxs)(`div`,{className:g(`max-w-[75%] space-y-1`,n?`items-end`:`items-start`,`flex flex-col`),children:[(0,b.jsxs)(`div`,{className:g(`flex items-center gap-2`,n?`flex-row-reverse`:`flex-row`),children:[(0,b.jsx)(`span`,{className:`text-[10px] font-bold text-shogun-text`,children:e.from_name}),(0,b.jsx)(`span`,{className:g(`text-[8px] font-bold px-1.5 py-0.5 rounded border uppercase tracking-widest`,t.color),children:t.label}),(0,b.jsx)(`span`,{className:`text-[9px] text-shogun-subdued/60 font-mono`,children:Se(e.created_at)})]}),(0,b.jsx)(`div`,{className:g(`px-4 py-2.5 rounded-2xl text-sm leading-relaxed`,n?`bg-indigo-600 text-white rounded-tr-sm`:`bg-shogun-card border border-shogun-border text-shogun-text rounded-tl-sm`),children:(0,b.jsx)(`span`,{dangerouslySetInnerHTML:{__html:e.content.replace(/\*\*(.+?)\*\*/g,`$1`).replace(/`(.+?)`/g,`$1`)}})}),n&&e.delivery_status!==`local`&&(0,b.jsx)(`span`,{className:g(`text-[8px] font-mono`,e.delivery_status===`delivered`?`text-green-400`:e.delivery_status===`failed`?`text-red-400`:`text-shogun-subdued`),children:e.delivery_status})]})]},e.id)}),(0,b.jsx)(`div`,{ref:it})]}),(0,b.jsxs)(`div`,{className:`p-4 border-t border-shogun-border bg-shogun-card space-y-3 flex-shrink-0`,children:[(0,b.jsx)(`div`,{className:`flex flex-wrap gap-1.5`,children:[`update`,`proposal`,`task`,`plan_revision`,`approval`,`signal`].map(e=>{let t=w[e];return(0,b.jsx)(`button`,{onClick:()=>Ne(e),className:g(`text-[9px] px-2.5 py-1 rounded border font-bold uppercase tracking-widest transition-all`,L===e?t.color:`text-shogun-subdued border-shogun-border hover:border-shogun-text`),children:t.label},e)})}),(0,b.jsxs)(`div`,{className:`flex gap-2`,children:[(0,b.jsx)(`textarea`,{value:I,onChange:e=>Me(e.target.value),placeholder:`Post a ${w[L]?.label||`message`}...`,rows:2,className:`flex-1 bg-[#050508] border border-shogun-border rounded-xl px-4 py-2.5 text-sm focus:border-indigo-500 outline-none transition-all resize-none`,onKeyDown:e=>{e.key===`Enter`&&e.ctrlKey&&st()}}),(0,b.jsx)(`button`,{onClick:st,disabled:Pe||!I.trim(),className:`px-4 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl transition-all disabled:opacity-40 flex items-center gap-2`,children:Pe?(0,b.jsx)(c,{className:`w-4 h-4 animate-spin`}):(0,b.jsx)(pe,{className:`w-4 h-4`})})]}),(0,b.jsx)(`p`,{className:`text-[9px] text-shogun-subdued/50`,children:`Ctrl+Enter to send · Messages are fanned out to all active peers`})]})]}),(0,b.jsxs)(`div`,{className:`overflow-y-auto flex flex-col`,children:[(0,b.jsxs)(`div`,{className:`p-4 border-b border-shogun-border flex items-center justify-between flex-shrink-0`,children:[(0,b.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,b.jsx)(ce,{className:`w-4 h-4 text-shogun-gold`}),(0,b.jsx)(`p`,{className:`text-[10px] font-bold text-shogun-text uppercase tracking-widest`,children:`Shared Document`}),r.document_version>0&&(0,b.jsxs)(`span`,{className:`text-[8px] font-mono text-shogun-subdued/60`,children:[`v`,r.document_version]})]}),Ie?(0,b.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,b.jsx)(`button`,{onClick:()=>R(!1),className:`p-1.5 text-shogun-subdued hover:text-shogun-text transition-colors`,children:(0,b.jsx)(m,{className:`w-3.5 h-3.5`})}),(0,b.jsxs)(`button`,{onClick:ct,disabled:B,className:`px-3 py-1.5 bg-shogun-gold hover:bg-shogun-gold/90 text-black text-[10px] font-bold uppercase tracking-widest rounded-lg transition-all flex items-center gap-1.5`,children:[B?(0,b.jsx)(c,{className:`w-3 h-3 animate-spin`}):(0,b.jsx)(fe,{className:`w-3 h-3`}),`Save`]})]}):(0,b.jsx)(`button`,{onClick:()=>{R(!0),z(r.shared_document||``)},className:`p-1.5 text-shogun-subdued hover:text-shogun-gold transition-colors rounded-lg hover:bg-shogun-card`,children:(0,b.jsx)(ue,{className:`w-3.5 h-3.5`})})]}),Ie?(0,b.jsx)(`textarea`,{value:Le,onChange:e=>z(e.target.value),className:`flex-1 bg-[#050508] text-sm text-shogun-text font-mono p-4 outline-none resize-none border-none`,placeholder:`# Document Title
-
-Write your shared plan here...`}):(0,b.jsx)(`div`,{className:`flex-1 p-4 overflow-y-auto`,children:r.shared_document?(0,b.jsx)(`div`,{className:`prose prose-invert prose-sm max-w-none text-shogun-text text-sm leading-relaxed space-y-3`,dangerouslySetInnerHTML:{__html:r.shared_document.replace(/^# (.+)$/gm,`$1`).replace(/\n/g,`$1`)}}),(0,S.jsx)(`div`,{className:`flex-1 h-px bg-shogun-border`})]},e.id):(0,S.jsxs)(`div`,{className:m(`flex gap-3`,n?`flex-row-reverse`:`flex-row`),children:[(0,S.jsx)(`div`,{className:m(`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 text-xs font-bold`,n?`bg-indigo-500/20 text-indigo-400`:`bg-shogun-card border border-shogun-border text-shogun-subdued`),children:(e.from_name||`?`).charAt(0).toUpperCase()}),(0,S.jsxs)(`div`,{className:m(`max-w-[75%] space-y-1`,n?`items-end`:`items-start`,`flex flex-col`),children:[(0,S.jsxs)(`div`,{className:m(`flex items-center gap-2`,n?`flex-row-reverse`:`flex-row`),children:[(0,S.jsx)(`span`,{className:`text-[10px] font-bold text-shogun-text`,children:e.from_name}),(0,S.jsx)(`span`,{className:m(`text-[8px] font-bold px-1.5 py-0.5 rounded border uppercase tracking-widest`,t.color),children:t.label}),(0,S.jsx)(`span`,{className:`text-[9px] text-shogun-subdued/60 font-mono`,children:Ce(e.created_at)})]}),(0,S.jsx)(`div`,{className:m(`px-4 py-2.5 rounded-2xl text-sm leading-relaxed`,n?`bg-indigo-600 text-white rounded-tr-sm`:`bg-shogun-card border border-shogun-border text-shogun-text rounded-tl-sm`),children:(0,S.jsx)(`span`,{dangerouslySetInnerHTML:{__html:e.content.replace(/\*\*(.+?)\*\*/g,`$1`).replace(/`(.+?)`/g,`$1`)}})}),n&&e.delivery_status!==`local`&&(0,S.jsx)(`span`,{className:m(`text-[8px] font-mono`,e.delivery_status===`delivered`?`text-green-400`:e.delivery_status===`failed`?`text-red-400`:`text-shogun-subdued`),children:e.delivery_status})]})]},e.id)}),(0,S.jsx)(`div`,{ref:lt})]}),(0,S.jsxs)(`div`,{className:`p-4 border-t border-shogun-border bg-shogun-card space-y-3 flex-shrink-0`,children:[(0,S.jsx)(`div`,{className:`flex flex-wrap gap-1.5`,children:[`update`,`proposal`,`task`,`plan_revision`,`approval`,`signal`].map(e=>{let t=E[e];return(0,S.jsx)(`button`,{onClick:()=>Re(e),className:m(`text-[9px] px-2.5 py-1 rounded border font-bold uppercase tracking-widest transition-all`,L===e?t.color:`text-shogun-subdued border-shogun-border hover:border-shogun-text`),children:t.label},e)})}),(0,S.jsxs)(`div`,{className:`flex gap-2`,children:[(0,S.jsx)(`textarea`,{value:I,onChange:e=>Le(e.target.value),placeholder:`Post a ${E[L]?.label||`message`}...`,rows:2,className:`flex-1 bg-[#050508] border border-shogun-border rounded-xl px-4 py-2.5 text-sm focus:border-indigo-500 outline-none transition-all resize-none`,onKeyDown:e=>{e.key===`Enter`&&e.ctrlKey&&ft()}}),(0,S.jsx)(`button`,{onClick:ft,disabled:ze||!I.trim(),className:`px-4 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl transition-all disabled:opacity-40 flex items-center gap-2`,children:ze?(0,S.jsx)(p,{className:`w-4 h-4 animate-spin`}):(0,S.jsx)(le,{className:`w-4 h-4`})})]}),(0,S.jsx)(`p`,{className:`text-[9px] text-shogun-subdued/50`,children:`Ctrl+Enter to send · Messages are fanned out to all active peers`})]})]}),(0,S.jsxs)(`div`,{className:`overflow-y-auto flex flex-col`,children:[(0,S.jsxs)(`div`,{className:`p-4 border-b border-shogun-border flex items-center justify-between flex-shrink-0`,children:[(0,S.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,S.jsx)(ae,{className:`w-4 h-4 text-shogun-gold`}),(0,S.jsx)(`p`,{className:`text-[10px] font-bold text-shogun-text uppercase tracking-widest`,children:`Shared Document`}),i.document_version>0&&(0,S.jsxs)(`span`,{className:`text-[8px] font-mono text-shogun-subdued/60`,children:[`v`,i.document_version]})]}),Be?(0,S.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,S.jsx)(`button`,{onClick:()=>z(!1),className:`p-1.5 text-shogun-subdued hover:text-shogun-text transition-colors`,children:(0,S.jsx)(f,{className:`w-3.5 h-3.5`})}),(0,S.jsxs)(`button`,{onClick:pt,disabled:He,className:`px-3 py-1.5 bg-shogun-gold hover:bg-shogun-gold/90 text-black text-[10px] font-bold uppercase tracking-widest rounded-lg transition-all flex items-center gap-1.5`,children:[He?(0,S.jsx)(p,{className:`w-3 h-3 animate-spin`}):(0,S.jsx)(ce,{className:`w-3 h-3`}),`Save`]})]}):(0,S.jsx)(`button`,{onClick:()=>{z(!0),B(i.shared_document||``)},className:`p-1.5 text-shogun-subdued hover:text-shogun-gold transition-colors rounded-lg hover:bg-shogun-card`,children:(0,S.jsx)(oe,{className:`w-3.5 h-3.5`})})]}),Be?(0,S.jsx)(`textarea`,{value:Ve,onChange:e=>B(e.target.value),className:`flex-1 bg-[#050508] text-sm text-shogun-text font-mono p-4 outline-none resize-none border-none`,placeholder:`# Document Title
+
+Write your shared plan here...`}):(0,S.jsx)(`div`,{className:`flex-1 p-4 overflow-y-auto`,children:i.shared_document?(0,S.jsx)(`div`,{className:`prose prose-invert prose-sm max-w-none text-shogun-text text-sm leading-relaxed space-y-3`,dangerouslySetInnerHTML:{__html:i.shared_document.replace(/^# (.+)$/gm,`$1`).replace(/\n/g,`