diff --git a/.env.example b/.env.example index 6512976..8d44d9c 100644 --- a/.env.example +++ b/.env.example @@ -45,7 +45,8 @@ GITHUB_TOKEN=your_github_personal_access_token # connectors for local development only, set this to 1 (logged as a warning). # CONCORD_ALLOW_INSECURE_TRANSPORT=1 -# ── Connector tokens ──────────────────────────────────────────────TERRASECURE_TOKEN= +# ── Connector tokens ────────────────────────────────────────────── +TERRASECURE_TOKEN= TRIVY_TOKEN= KAGENT_TOKEN= HOLMESGPT_TOKEN= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb04903..7bebfac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,14 +6,34 @@ on: pull_request: branches: [main, develop] +# Least privilege: this workflow only needs to read the repo. +permissions: + contents: read + +# Cancel superseded runs on the same ref to save minutes. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" - - run: pip install -r requirements/dev.txt - - run: ruff check . - - run: pytest tests/unit -v --tb=short + cache: pip + cache-dependency-path: requirements/dev.txt + + - name: Install dependencies + run: pip install -r requirements/dev.txt "psycopg[binary,pool]" + + - name: Lint + run: ruff check . + + - name: Test (full suite, excluding slow) + env: + CONCORD_DB_PATH: ":memory:" + run: pytest tests/ -q -m "not slow" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e5c9ce..148d1e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,10 +4,39 @@ on: push: tags: ["v*"] +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + jobs: release: runs-on: ubuntu-latest + permissions: + contents: write # create the GitHub release + packages: write # push the image to GHCR steps: - uses: actions/checkout@v4 - - run: docker build -t ghcr.io/beyondbug/concord:${{ github.ref_name }} . - - uses: softprops/action-gh-release@v2 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ghcr.io/beyondbug/concord:${{ github.ref_name }} + ghcr.io/beyondbug/concord:latest + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true \ No newline at end of file diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 5985c6b..b0451f0 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -5,25 +5,49 @@ on: branches: [main, develop] pull_request: +# Default to read-only; the SARIF upload job elevates only what it needs. permissions: contents: read - security-events: write - actions: read + +concurrency: + group: security-${{ github.ref }} + cancel-in-progress: true jobs: trivy: runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # required to upload SARIF to code scanning steps: - uses: actions/checkout@v4 - - name: Run Trivy filesystem scan - uses: aquasecurity/trivy-action@master + + - name: Trivy filesystem scan + # Pinned to a released tag rather than @master for supply-chain safety. + uses: aquasecurity/trivy-action@0.35.0 with: scan-type: fs scan-ref: . format: sarif output: trivy.sarif + severity: CRITICAL,HIGH + - name: Upload SARIF uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy.sarif continue-on-error: true + + pip-audit: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Audit Python dependencies + run: | + pip install pip-audit + pip-audit -r requirements/base.txt \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ee4e2c2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +All notable changes to Concord are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/), and the project aims to follow +semantic versioning once it reaches a tagged release. + +## [Unreleased] + +### Added +- **SecurityPolicyAgent** with a dependency-free source-code pattern scanner + (Python/JS/TS/Go/PHP). +- **Durable persistence**: SQLite by default, optional **PostgreSQL** backend + selected via `CONCORD_DATABASE_URL` with graceful fallback and schema + migration. +- **API authentication** (API key, fail-safe dev mode) and request + **correlation IDs** threaded into logs and the audit trail. +- **MCP transport hardening**: TLS verification by default, CA bundles, mTLS, + and refusal of plaintext URLs unless explicitly opted in. +- **Prompt-injection sanitization** of untrusted tool output before it reaches + the LLM prompt. +- **Redis-backed dedup** with an in-memory TTL fallback. +- **Approval lifecycle**: approve, reject, and expire flows — all durable and + audited — plus a pending-approvals queue. +- **Structured (JSON) logging** mode. +- **Web dashboard** with seven live views: Overview, Findings, Incidents, Security, + Approvals, Audit, Settings (all read from the live API; no mock data). +- **CLI** with human and `--json` output: `health`, `findings`, `audit`, + `approvals`, `approve`, `reject`, `stats`, `diagnostics`, `invoke`, `agents`, + and shell-completion help. +- **Docker** multi-stage non-root image with a health check; hardened + `docker-compose` (loopback ports, read-only rootfs, required secrets, + service health checks). +- **Helm chart** with security context, resource limits, probes, and a + least-privilege service account. +- **Documentation**: rewritten `README.md`, `docs/ARCHITECTURE.md`, and + `docs/threat-model.md`. +- **CI/CD hardening**: least-privilege workflow permissions, full-suite CI, + pinned actions, and a dependency-audit job. + +### Changed +- Confidence scoring remains deterministic + (`severity_weight × source_reliability`), never LLM self-reported. +- Audit records are now durable and correlation-tagged (previously log-only). + +### Security +- Per-connector scoped credentials; no master credential. +- Human approval gate for consequential/tie-break outcomes. + +### Known limitations +- Kubernetes and Observability agents are scaffolded but require live MCP + services (kagent / HolmesGPT) to complete. +- Terraform assets under `infra/` remain placeholders. \ No newline at end of file diff --git a/README.md b/README.md index 0c0b86b..0363eb3 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,277 @@

One AI brain across your entire DevSecOps stack.

+ +

+ A self-hosted platform that triages security findings, runs domain agents, + arbitrates their results with a deterministic confidence model, and keeps a + human in the loop for consequential actions — with a full audit trail. +

+ +--- + +## What Concord is + +Concord ingests security findings (from CI/CD, IaC scans, webhooks, or its own +scanners), decides whether each one is trivial enough to fast-path or needs +deeper analysis, runs the relevant **domain agents**, and **arbitrates** their +competing conclusions using a deterministic confidence score. Low-confidence +ties are escalated to a **human approval gate** rather than auto-resolved. +Every decision — fast-path or AI-path, approval or rejection — is written to a +durable, correlation-tagged **audit trail**. + +It is designed to orchestrate existing security tools over the Model Context +Protocol (MCP), not replace them. + +### Why it exists + +Security teams drown in findings from a dozen disconnected tools, each with its +own console and confidence heuristics. Concord gives them one triage brain: a +consistent, auditable pipeline that decides what matters, explains why, and only +interrupts a human when a decision genuinely needs one. + +--- + +## Key principles (enforced in code and tests) + +- **Deterministic confidence** — `confidence = severity_weight × source_reliability`. + Never LLM self-reported. This is what arbitration ranks on. +- **Everything is audited** — every finding, including fast-path, produces an + audit record. Human approvals and rejections are audited too. +- **Scoped credentials** — per-connector tokens via the credential broker; no + master credential. +- **Swappable LLM** — the provider is chosen by `LLM_PROVIDER`; provider details + never leak into the orchestration logic. +- **Human-in-the-loop** — consequential/tie-break outcomes require explicit + human approval; nothing destructive happens silently. + +--- + +## Architecture + +```mermaid +flowchart TD + U[User / CI / Webhook] -->|finding| API[FastAPI API] + CLI[Concord CLI] --> API + DASH[Web Dashboard] --> API + API --> ORCH[Orchestrator] + ORCH --> TRIAGE{Triage gate} + TRIAGE -->|trivial| FAST[Fast path] + TRIAGE -->|needs analysis| AGENTS[Domain agents] + AGENTS --> INFRA[Infra] + AGENTS --> CICD[CI/CD] + AGENTS --> SEC[Security] + AGENTS -. planned .-> K8S[Kubernetes ·MCP] + AGENTS -. planned .-> OBS[Observability ·MCP] + INFRA & CICD & SEC --> ARB[Arbitration] + ARB -->|clear winner| RESOLVE[Auto-resolve] + ARB -->|close call| APPROVE[Human approval gate] + FAST & RESOLVE & APPROVE --> STORE[(Persistence·SQLite/Postgres)] + STORE --> AUDIT[(Audit trail)] + ORCH --> LLM[LLM provider ·swappable] +``` + +### Layers + +| Layer | Location | Responsibility | +|-------|----------|----------------| +| MCP runtime | `core/mcp_runtime/` | Transport (TLS/mTLS), registry, audit | +| Orchestrator | `core/orchestrator/` | Triage → agents → arbitration → LLM → store | +| Domain agents | `agents//` | Each wraps a backing scan/tool with a contract | +| Connectors | `connectors/tools.yaml` | Declarative external tools, orchestrated not replaced | + +### Request/execution flow + +1. A finding arrives (API, webhook, CLI, or a scan). +2. The **triage gate** applies rules (severity, known patterns, dedup). Trivial + findings take the **fast path** — no LLM call. +3. Otherwise domain agents analyze it; each returns a structured result and a + deterministic confidence score. +4. **Arbitration** ranks the agents. A clear winner auto-resolves; a close call + (small confidence gap) becomes a **pending approval**. +5. The result and an **audit record** (tagged with the request correlation ID) + are persisted. +6. A human approves or rejects pending items; stale ones can be expired. + +--- + +## Features + +- **Triage + arbitration** with a deterministic confidence model. +- **Domain agents**: Infra (Terraform/pattern scan), CI/CD (K8s/Dockerfile), + Security (source-code pattern scan). Kubernetes and Observability agents are + **scaffolded and planned** — they require live MCP services (kagent / + HolmesGPT) and are not yet wired end-to-end. +- **Persistence**: durable SQLite by default; **PostgreSQL** backend selected + automatically when `CONCORD_DATABASE_URL` is set, with graceful fallback. +- **Auditable approvals**: approve, reject, and expire flows — all durable and + audited; a pending-approvals queue. +- **Security**: API-key auth (fail-safe dev mode), TLS/mTLS transport, + prompt-injection sanitization on untrusted tool output, dedup. +- **Observability**: structured (JSON) logging and request **correlation IDs** + threaded from the API into logs and audit records. +- **Interfaces**: a web dashboard (Overview / Findings / Approvals / Audit) and + a CLI with human and `--json` machine-readable output. + +--- + +## Quickstart + +### Local (Python) + +```bash +python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1 +pip install -r requirements/dev.txt +cp .env.example .env # then edit as needed + +uvicorn api.main:app --reload --port 8000 +# open http://localhost:8000 (dashboard) +``` + +Trigger a demo finding through the pipeline: + +```bash +python concord_cli/main.py invoke -s CRITICAL +python concord_cli/main.py findings --json +python concord_cli/main.py audit +``` + +### Docker + +```bash +# Set POSTGRES_PASSWORD in .env first (required; no weak default). +docker compose up --build +``` + +The image is multi-stage, runs as a non-root user, and has a health check. + +--- + +## Configuration + +All configuration is via environment variables (see `.env.example`). Highlights: + +| Variable | Purpose | +|----------|---------| +| `LLM_PROVIDER` | Which LLM backend to use (swappable). | +| `CONCORD_API_KEY` | Enables API auth. Unset = open dev mode (logged). | +| `CONCORD_DB_PATH` | SQLite path (default backend). | +| `CONCORD_DATABASE_URL` / `POSTGRES_URL` | Use PostgreSQL; falls back to SQLite if unreachable. | +| `REDIS_URL` | Enables Redis-backed dedup; falls back to in-memory. | +| `CONCORD_LOG_FORMAT` | `json` for structured logs, else human text. | +| `CONCORD_ALLOW_INSECURE_TRANSPORT` | Allow plaintext MCP URLs (dev only). | +| `WEBHOOK_SECRET` | HMAC secret for the GitHub webhook. | + +> If `POSTGRES_URL` is set but no database is running, the app falls back to +> SQLite **at startup** (one short timeout at boot). Comment it out to skip +> Postgres entirely. + +--- + +## CLI + +``` +concord health # API health + auth posture +concord findings [--json] # recent findings + stats +concord audit [--json] # audit trail with correlation IDs +concord approvals # pending human decisions +concord approve # resolve a tie-break +concord reject # reject a pending finding +concord invoke -s CRITICAL # run the pipeline on a demo finding +concord agents # domain agents + status +``` + +`--json` (or `CONCORD_OUTPUT=json`) emits pure JSON for scripting; colour is +disabled automatically when output is piped. + +--- + +## API + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/health` | Liveness + auth posture | +| GET | `/findings/` | Findings + stats | +| GET | `/findings/{id}` | One finding | +| GET | `/audit/` | Audit trail | +| GET | `/events/approvals/pending` | Pending approvals | +| POST | `/events/findings/{id}/approve/{agent}` | Approve a tie-break | +| POST | `/events/findings/{id}/reject` | Reject a finding | +| POST | `/events/approvals/expire` | Expire stale approvals | +| POST | `/events/demo` | Run the pipeline on a demo finding | +| POST | `/events/scan-crms` | Trigger a repository scan | +| POST | `/events/github` | GitHub webhook (HMAC-verified) | + +Data/state routes require the API key when `CONCORD_API_KEY` is set. `/health`, +`/`, and the HMAC-verified webhook are public by design. + +--- + +## Deployment (Kubernetes / Helm) + +```bash +helm lint helm/concord +helm template concord helm/concord | kubectl apply -f - +``` + +The chart ships hardened defaults: non-root pod/container security context, +read-only root filesystem, dropped capabilities, resource requests/limits, +liveness/readiness probes, and a least-privilege service account with the token +not mounted. Provide secrets via a Kubernetes Secret referenced by +`envFromSecret`. + +--- + +## Testing + +```bash +ruff check . +pytest tests/ -q -m "not slow" # fast suite +pytest -m slow # slow/real-timeout tests +``` + +The suite covers triage, arbitration, agents, persistence (SQLite + Postgres +logic + migration), auth, transport security, sanitization, dedup, the approval +lifecycle, observability/correlation, and the CLI. + +--- + +## Project structure + +``` +api/ FastAPI app, routes, middleware, dashboard +agents/ domain agents (infra, cicd, security, k8s*, observability*) +core/ + orchestrator/ triage → agents → arbitration → LLM → store + arbitration/ deterministic confidence + resolver + triage/ gate + rules (severity, patterns, dedup) + persistence/ SQLite + PostgreSQL stores + mcp_runtime/ transport (TLS/mTLS), registry, audit + observability/ correlation IDs + structured logging + credential_broker/ scoped per-connector tokens +concord_cli/ Typer CLI +connectors/ tools.yaml manifest +helm/ infra/ deployment assets +tests/ unit + integration +docs/ architecture, completion tracker +``` +`*` scaffolded / planned (requires live MCP services). + +--- + +## Status + +Concord is under active development. The triage/arbitration core, persistence, +approvals, security controls, observability, CLI, and dashboard read views are +implemented and tested. The Kubernetes and Observability agents are scaffolded +but require live MCP endpoints to complete. See `docs/PROJECT_COMPLETION.md` for +a slice-by-slice status. + +## Contributing + +See `CONTRIBUTING.md` and `DEVELOPMENT.md`. Before changing core behaviour, read +`CLAUDE.md` — it records the invariants above that must not be broken casually. + +## License + +See `LICENSE`. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md index 51ee511..1dea709 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,4 +14,4 @@ See docs/threat-model.md for the full threat model. - No master credential. Per-connector scoped tokens only. - Rollback recommendations always require human approval. - Every action (including fast-path) logged to audit trail. -- Tool output sanitized before LLM context injection (Phase 3). +- Tool output sanitized before LLM context injection. \ No newline at end of file diff --git a/api/main.py b/api/main.py index dd02b5d..1d9bbc6 100644 --- a/api/main.py +++ b/api/main.py @@ -56,4 +56,4 @@ def health(): "service": "concord", "version": "0.1.0", "auth_enforced": auth_is_enforced(), - } \ No newline at end of file + } diff --git a/api/middleware/logging.py b/api/middleware/logging.py index a6a84d0..63d3eff 100644 --- a/api/middleware/logging.py +++ b/api/middleware/logging.py @@ -49,4 +49,4 @@ async def dispatch(self, request: Request, call_next): response.status_code, duration_ms, ) response.headers[_REQUEST_ID_HEADER] = request_id - return response \ No newline at end of file + return response diff --git a/api/routes/agents.py b/api/routes/agents.py new file mode 100644 index 0000000..fbed268 --- /dev/null +++ b/api/routes/agents.py @@ -0,0 +1,51 @@ +""" +api/routes/agents.py +Agent metadata endpoint — single source of truth for the UI/CLI. + +Status is derived honestly: an agent is "active" only if its analyze() is +implemented (does not raise NotImplementedError). The Kubernetes and +Observability agents are scaffolded and report "planned" until their MCP +backends are wired in. +""" +from fastapi import APIRouter + +from core.models.agent_response import SOURCE_RELIABILITY + +router = APIRouter(prefix="/agents", tags=["agents"]) + +# Human-facing backing description per domain. Kept here (not fabricated from +# the model) so the label matches what actually runs. +_BACKING = { + "infra": "TerraSecure pattern scanner", + "cicd": "Trivy · Checkov", + "security": "source-code pattern scan", + "kubernetes": "kagent (MCP)", + "observability": "HolmesGPT (MCP)", +} + +# Active = analyze() implemented. Planned = scaffolded, needs a live MCP service. +_ACTIVE = {"infra", "cicd", "security"} + + +def _agent_list() -> list[dict]: + out = [] + for domain, reliability in SOURCE_RELIABILITY.items(): + out.append({ + "domain": domain, + "reliability": reliability, + "backing": _BACKING.get(domain, domain), + "status": "active" if domain in _ACTIVE else "planned", + }) + # Stable order: active first (by reliability desc), then planned. + out.sort(key=lambda a: (a["status"] != "active", -a["reliability"])) + return out + + +@router.get("/") +async def list_agents(): + agents = _agent_list() + return { + "agents": agents, + "active": sum(1 for a in agents if a["status"] == "active"), + "planned": sum(1 for a in agents if a["status"] == "planned"), + } \ No newline at end of file diff --git a/api/routes/events.py b/api/routes/events.py index 1018fb3..ac7d9e3 100644 --- a/api/routes/events.py +++ b/api/routes/events.py @@ -1,15 +1,17 @@ """ api/routes/events.py -Webhook receiver + /demo endpoint for Friday review. +Webhook receiver + /demo endpoint + approval lifecycle endpoints. """ import hashlib import hmac import logging import os +from datetime import UTC, datetime, timedelta from fastapi import APIRouter, BackgroundTasks, HTTPException, Request from core.models.finding import Finding +from core.persistence.store import AuditRecord, get_store router = APIRouter(prefix="/events", tags=["events"]) logger = logging.getLogger("concord.events") @@ -58,7 +60,6 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks): async def demo_endpoint(severity: str = "CRITICAL"): """ Demo endpoint — run a sample finding through Concord synchronously. - Perfect for the Friday review: hit this from /docs and see the full result. Try: POST /events/demo?severity=CRITICAL POST /events/demo?severity=LOW @@ -78,3 +79,116 @@ async def demo_endpoint(severity: str = "CRITICAL"): ) return await Orchestrator().process(finding) + + +@router.get("/approvals/pending") +async def list_pending_approvals(limit: int = 20): + """List findings awaiting a human approval decision.""" + store = get_store() + pending = store.list_pending_approvals(limit=limit) + return {"pending": pending, "total": len(pending)} + + +@router.post("/findings/{finding_id}/approve/{agent}") +async def approve_finding(finding_id: str, agent: str): + """Approve a finding's tiebreak by selecting the winning agent.""" + store = get_store() + record = store.get_finding(finding_id) + if record is None: + raise HTTPException(status_code=404, detail=f"Finding {finding_id!r} not found") + + result = record.get("result", {}) + + # Validate agent is one of the candidates that actually ran + candidate_agents = result.get("agents", {}) + if candidate_agents and agent not in candidate_agents: + raise HTTPException( + status_code=400, + detail=f"Agent {agent!r} is not a candidate for finding {finding_id!r}. " + f"Valid agents: {list(candidate_agents.keys())}", + ) + + result["approved_by"] = agent + result["auto_resolved"] = True + persisted = store.update_finding_result(finding_id, result) + + # Audit reason format must match: "human_approved:{agent}" (no space) + store.add_audit(AuditRecord( + finding_id=finding_id, + path=record.get("path", ""), + reason=f"human_approved:{agent}", + agent=agent, + )) + + github_url = result.get("github_url") + return { + "status": "approved", + "finding_id": finding_id, + "agent": agent, + "persisted": persisted, + **({"github_url": github_url} if github_url else {}), + } + + +@router.post("/findings/{finding_id}/reject") +async def reject_finding(finding_id: str, reason: str = "rejected"): + """Reject a finding — marks it resolved so it leaves the pending queue.""" + store = get_store() + record = store.get_finding(finding_id) + if record is None: + raise HTTPException(status_code=404, detail=f"Finding {finding_id!r} not found") + + result = record.get("result", {}) + result["rejected"] = True + result["reject_reason"] = reason + result["approved_by"] = "__rejected__" + result["auto_resolved"] = True + persisted = store.update_finding_result(finding_id, result) + + store.add_audit(AuditRecord( + finding_id=finding_id, + path=record.get("path", ""), + reason=f"human_rejected:{reason}", + agent=None, + )) + + return { + "status": "rejected", + "finding_id": finding_id, + "reason": reason, + "persisted": persisted, + } + + +@router.post("/approvals/expire") +async def expire_old_approvals(max_age_hours: int = 24): + """Expire pending approvals older than max_age_hours; audits each one.""" + store = get_store() + pending = store.list_pending_approvals(limit=500) + cutoff = datetime.now(UTC) - timedelta(hours=max_age_hours) + expired = [] + + for record in pending: + ts_str = record.get("timestamp", "") + try: + ts = datetime.fromisoformat(ts_str) + if ts.tzinfo is None: + ts = ts.replace(tzinfo=UTC) + except ValueError: + continue + + if ts < cutoff: + finding_id = record["id"] + result = record.get("result", {}) + result["approved_by"] = "__expired__" + result["auto_resolved"] = True + store.update_finding_result(finding_id, result) + store.add_audit(AuditRecord( + finding_id=finding_id, + path=record.get("path", ""), + reason=f"approval_expired:{max_age_hours}h", + agent=None, + )) + expired.append(finding_id) + + return {"status": "ok", "count": len(expired), "expired": expired} diff --git a/api/routes/findings.py b/api/routes/findings.py index cd4507c..17115dd 100644 --- a/api/routes/findings.py +++ b/api/routes/findings.py @@ -1,11 +1,6 @@ """ api/routes/findings.py Findings REST API backed by the persistent store (core.persistence). - -``store`` is kept as a thin adapter with the historical method names -(``add``/``get``/``all``/``stats``) so existing callers such as -api/routes/scan.py keep working, but every call now reads and writes the -durable SQLite-backed store instead of an in-memory list. """ import os @@ -32,8 +27,9 @@ def add(self, finding_id: str, severity: str, artifact: str, result=result, )) - def all(self, limit: int = 50) -> list: - return get_store().list_findings(limit=limit) + def all(self, limit: int = 50, severity: str | None = None, + path: str | None = None) -> list: + return get_store().list_findings(limit=limit, severity=severity, path=path) def get(self, finding_id: str) -> dict | None: return get_store().get_finding(finding_id) @@ -46,11 +42,44 @@ def stats(self) -> dict: @router.get("/") -async def list_findings(limit: int = 50): +async def list_findings(limit: int = 50, severity: str | None = None, + path: str | None = None): return { - "findings": store.all(limit), + "findings": store.all(limit, severity=severity, path=path), "stats": store.stats(), "llm_provider": os.getenv("LLM_PROVIDER", "ollama"), + "filters": {"severity": severity, "path": path}, + } + + +@router.get("/severity") +async def severity_breakdown(): + """Findings grouped by severity (for the Security view).""" + return {"by_severity": get_store().severity_breakdown()} + + +@router.get("/incidents") +async def incidents(limit: int = 50): + """Findings grouped by affected artifact into incident summaries.""" + inc = get_store().incidents(limit=limit) + return {"incidents": inc, "total": len(inc)} + + +@router.get("/{finding_id}/detail") +async def finding_detail(finding_id: str): + """A finding joined with its audit timeline (for the detail panel).""" + s = get_store() + f = s.get_finding(finding_id) + if not f: + raise HTTPException(status_code=404, detail="Finding not found") + result = f.get("result", {}) + return { + "finding": f, + "agents": result.get("agents", {}), + "auto_resolved": result.get("auto_resolved"), + "approved_by": result.get("approved_by"), + "rejected": result.get("rejected", False), + "timeline": s.audit_for_finding(finding_id), } @@ -59,4 +88,4 @@ async def get_finding(finding_id: str): f = store.get(finding_id) if not f: raise HTTPException(status_code=404, detail="Finding not found") - return f \ No newline at end of file + return f diff --git a/api/templates/dashboard.html b/api/templates/dashboard.html index f378bcb..77a6f0f 100644 --- a/api/templates/dashboard.html +++ b/api/templates/dashboard.html @@ -4,6 +4,7 @@ Concord — AI DevSecOps Platform +