> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pipefort.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API reference

> The Go HTTP endpoints under /api/*.

The Pipefort API exposes eleven endpoints under `/api/*`.

All non-health endpoints require a **Supabase JWT** in the `Authorization` header:

```
Authorization: Bearer <supabase-access-token>
```

The token is the access token the SPA gets from supabase-js after a successful GitHub login. The API verifies it (HS256, `SUPABASE_JWT_SECRET`) and extracts `user_id` for downstream queries.

## `GET /api/health`

Unauthenticated health check. Returns `{ "status": "ok" }`.

## `GET /api/repos`

Refreshes the user's repositories from GitHub (across **all** their installations) and persists them. Returns the stored rows.

**Response**

```json theme={null}
{
  "connected": true,
  "repositories": [
    {
      "id": "uuid",
      "full_name": "owner/repo",
      "private": false,
      "html_url": "https://github.com/owner/repo"
    }
  ]
}
```

If the user hasn't connected any GitHub App installations yet, returns `{ "connected": false, "repositories": [] }`.

## `POST /api/scan`

Scans a single repository. The client loops this across repos client-side for org-wide scans, keeping each request short.

**Request**

```json theme={null}
{
  "repo_id": "uuid-from-GET-/api/repos",
  "ruleset": "all"        // or "owasp"
}
```

`ruleset` defaults to `"all"` if omitted.

**Response**

```json theme={null}
{
  "scan_id": "uuid",
  "repo_id": "uuid",
  "counts": {
    "HIGH": 2,
    "MEDIUM": 1,
    "LOW": 0,
    "INFO": 0
  },
  "findings": [
    {
      "file": ".github/workflows/release.yml",
      "line": 12,
      "column": 5,
      "severity": "HIGH",
      "category": "CICD-SEC-4",
      "rule_id": "cicd-sec-4-ppe-shell-injection",
      "title": "Poisoned Pipeline Execution (Shell Injection)",
      "description": "...",
      "recommendation": "..."
    }
  ]
}
```

The `findings` array uses the same shape as the CLI's JSON output — they share `scanner.Finding`. The `rule_id` field is the stable per-check identifier the [Rule settings](/webapp/rule-settings) page toggles on; `category` is the coarser OWASP / best-practice group several rules can share.

The findings returned and the severity counts both reflect any [rule preferences](/webapp/rule-settings) the user has set — filtering happens server-side before persistence, so `findings` and `counts` always agree.

## `GET /api/attack-paths`

Returns [toxic combinations](/concepts/attacker-mind) across every repository
the user owns, computed from each repo's latest-scan findings. Powers the
[Attacker Mind dashboard](/webapp/attacker-mind). Repositories with no
combinations are omitted.

**Response**

```json theme={null}
{
  "repos": [
    {
      "repo_id": "uuid",
      "full_name": "acme/demo",
      "html_url": "https://github.com/acme/demo",
      "combos": [
        {
          "id": "pwn-request",
          "title": "Pwn Request — untrusted PR code runs with a writable token",
          "severity": "CRITICAL",
          "scope": "file",
          "file": ".github/workflows/ci.yml",
          "impact": "...",
          "break_chain": "...",
          "break_chain_rule": "cicd-sec-1-ppe-checkout",
          "stages": [
            { "order": 0, "title": "...", "description": "...", "rule_id": "cicd-sec-1-ppe-checkout" }
          ],
          "components": [
            { "rule_id": "cicd-sec-1-ppe-checkout", "finding": { "...": "..." } }
          ]
        }
      ]
    }
  ],
  "total_combos": 1,
  "critical_count": 1,
  "high_count": 0
}
```

Detection runs the shared `pkg/scanner` engine — identical to the CLI — so a
combination never includes a finding from a rule disabled in
[Rule settings](/webapp/rule-settings).

## `GET /api/repositories/{repo_id}/combos`

Returns the [toxic combinations](/concepts/attacker-mind) for a **single**
repository, computed from its latest-scan findings — the per-repo counterpart of
`/api/attack-paths`. Powers the **Attacker Mind** card on the
[repository detail page](/webapp/overview). The user must own the repo (`404`
otherwise). `combos` is never `null` — an empty array means no combinations (and
also covers a repo that has never been scanned).

**Response**

```json theme={null}
{
  "combos": [
    {
      "id": "pwn-request",
      "title": "Pwn Request — untrusted PR code runs with a writable token",
      "severity": "CRITICAL",
      "scope": "file",
      "file": ".github/workflows/ci.yml",
      "impact": "...",
      "break_chain": "...",
      "break_chain_rule": "cicd-sec-1-ppe-checkout",
      "stages": [
        { "order": 0, "title": "...", "description": "...", "rule_id": "cicd-sec-1-ppe-checkout" }
      ],
      "components": [
        { "rule_id": "cicd-sec-1-ppe-checkout", "finding": { "...": "..." } }
      ]
    }
  ],
  "critical_count": 1,
  "high_count": 0
}
```

Like `/api/attack-paths`, detection respects the user's current
[rule settings](/webapp/rule-settings) — globals merged with this repo's
overrides — so a rule turned off after the last scan can't still form a
combination.

## `GET /api/github/callback`

Links a freshly-created GitHub App installation to the signed-in user. GitHub redirects here with `?installation_id=...`; the SPA forwards it with the user's bearer token so the API can associate the two.

**Query params**

| Param             | Required | Description                                                  |
| ----------------- | -------- | ------------------------------------------------------------ |
| `installation_id` | yes      | The numeric installation ID GitHub provides on the redirect. |

**Response**

```json theme={null}
{
  "installation_id": 12345,
  "account": "octocat",
  "account_type": "User"
}
```

## Rule settings

The seven endpoints below back the [Rule settings](/webapp/rule-settings) page (global toggles) and the **Rule overrides** card on each repository detail page (per-repo overrides). Reads return **sparse** rows — rules with no entry are treated as default-enabled per the catalog.

### `GET /api/rules`

Returns the canonical rule catalog (static — same data the SPA renders).

```json theme={null}
{
  "rules": [
    {
      "id": "best-prac-2-missing-timeout",
      "category": "BEST-PRAC-2",
      "title": "Job timeout not configured",
      "default_severity": "LOW",
      "surface": "workflow",
      "description": "...",
      "doc_url": "/rules/best-prac-2"
    }
  ]
}
```

`surface` is `"workflow"` or `"repo-settings"`.

### `GET /api/rule-settings`

Returns the user's sparse global preferences. Missing rule IDs are default-enabled.

```json theme={null}
{
  "settings": [
    { "rule_id": "best-prac-2-missing-timeout", "enabled": false, "updated_at": "..." }
  ]
}
```

### `PUT /api/rule-settings/{rule_id}`

Upserts a global preference. Returns `204 No Content`. Body:

```json theme={null}
{ "enabled": false }
```

Validates `rule_id` against the catalog; unknown slugs return `400`.

### `DELETE /api/rule-settings/{rule_id}`

Clears the global row, reverting the rule to default-enabled. Idempotent (`204` even if no row existed).

### `GET /api/repositories/{repo_id}/rule-overrides`

Returns the sparse list of override rows for one repo. The user must own the repo (verified via the same path `POST /api/scan` uses) — `404` otherwise.

```json theme={null}
{
  "overrides": [
    { "rule_id": "best-prac-2-missing-timeout", "enabled": true, "updated_at": "..." }
  ]
}
```

### `PUT /api/repositories/{repo_id}/rule-overrides/{rule_id}`

Upserts an override for `(user, repo, rule)`. Body `{ "enabled": bool }`. Returns `204`.

### `DELETE /api/repositories/{repo_id}/rule-overrides/{rule_id}`

Clears the override, reverting the rule to inherit from the global setting. Idempotent.

## Reading data directly (no API call)

Reading scans, findings, repositories, installations, and `rule_settings` does **not** go through the Go API — the SPA queries Postgres directly via supabase-js. Row-level security (`auth.uid() = user_id`) scopes every result to the signed-in user.

The API only handles **writes** (persisting scan results and rule preferences), **GitHub-side reads** (which need an installation token), and the **installation link** callback.
