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

# GitHub and GitLab Connectors: Source Control Research

> Configure Cynative's GitHub and GitLab connectors: token discovery, read-only enforcement, permission ceilings, and GitLab self-managed host configuration.

Cynative's GitHub and GitLab connectors are built for read-oriented source-control and repository security research. Both use an in-process per-request classifier to resolve each request to its API category and required access level, then allow it only within the configured ceiling — read-only by default, with certain sensitive categories blocked.

***

## GitHub connector

**Connector id:** `github`\
**Fixed host:** `api.github.com` (GitHub Enterprise Server is not supported)

### Quick start

Authenticate the GitHub CLI, then run Cynative from the same environment:

```bash theme={null}
gh auth login
cynative -p "review my GitHub repositories for exposed secrets"
```

No YAML configuration is required for the default read-only mode.

### Credential discovery

Cynative asks the GitHub CLI auth library ([go-gh](https://github.com/cli/go-gh)) for a token for `github.com`. Cynative does not read these variables itself; go-gh does. Because Cynative always queries the literal `github.com` host, go-gh's enterprise / `GH_HOST` branches never apply.

* `GH_TOKEN` — highest-precedence GitHub token; read by go-gh for `github.com`.
* `GITHUB_TOKEN` — second-precedence token (also the GitHub Actions built-in token).
* `GH_PATH` — path to the `gh` executable go-gh shells out to for the keyring-fallback token lookup.
* `GH_CONFIG_DIR` — directory go-gh reads the `gh auth login` token (`hosts.yml`) from.

**Registration and validation:** If no token is available, the connector is not registered and nothing is printed. When a token is present, Cynative eagerly validates it at startup with a hardened `GET /user` request (yielding the `@login` identity shown in the startup inventory). When `/user` is inconclusive — a GitHub App installation token, an Actions `GITHUB_TOKEN`, or a rate-limit condition — Cynative falls back to `GET /rate_limit`, a universally-accessible, rate-limit-exempt endpoint.

**References:**

* [GitHub CLI environment variables](https://cli.github.com/manual/gh_help_environment) — `GH_TOKEN` / `GITHUB_TOKEN`, `GH_PATH`, `GH_CONFIG_DIR`.
* [Authenticating to the GitHub REST API](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api) — fine-grained vs classic personal access tokens.

### Target selection

The GitHub connector always targets the public GitHub REST API at `api.github.com` — the host is hardcoded. It additionally authorizes the GitHub-owned download hosts `codeload.github.com`, `release-assets.githubusercontent.com`, and `objects.githubusercontent.com` (GET/HEAD only) so release-asset and tarball redirects can be followed.

Change the identity by changing the token: run `gh auth login`, or export `GH_TOKEN` (highest precedence) or `GITHUB_TOKEN`. The host is not configurable.

### Request usage

Use `auth_provider: "github"`. No connector-specific auth object is required.

```javascript theme={null}
const resp = await http_request({
  method: "GET",
  url: "https://api.github.com/user",
  headers: [],
  body: "",
  auth_provider: "github"
});
console.log(resp.body);
```

### Hardening

#### Host pinning

Host pinning allows only `api.github.com` and the three GitHub-owned download hosts. The download hosts are GET/HEAD-only, regardless of the configured permissions. Redirects are never followed — a 3xx is returned to the model, which must request the `Location` URL explicitly (re-entering host pinning and action authorization on each hop).

#### Category + access-level classification

The in-process classifier resolves each REST request to its GitHub category/subcategory and the required access level (read or write), then allows it only when the configured `connectors.github.permissions` ceiling permits that level for that category.

* The check runs **before** the token is attached, so a disallowed request is rejected without the credential ever leaving the process.
* The classifier is backed by GitHub's first-party OpenAPI description, distilled into a category routing table, fetched anonymously and cached from `raw.githubusercontent.com`.
* A REST request that cannot be classified fails closed. The `secret-scanning` category is denied by the secure baseline even for reads, because its read endpoints can leak secret material.
* The GraphQL API (`/graphql`) is not supported and is always denied.

#### Secret-scanning protection

Secret-scanning protection is layered: the table admission guard rejects any fetched spec that maps a `secret-scanning` template to another category; the `secret-scanning: none` baseline in the default exposure blocks all such requests at the ceiling; and unknown routes fail closed.

### Cynative configuration

`connectors.github.permissions` is a `category[/subcategory] → read|write|none` ceiling. Omit it entirely for the secure default (read everything except `secret-scanning`). The most-specific match wins (`category/subcategory` → `category` → `default`). `write` implies `read`; `none` denies even reads.

```yaml theme={null}
connectors:
  github:
    permissions:
      default: read            # baseline for unlisted categories
      issues: write            # allow creating/editing issues
      pulls: write             # allow opening/merging PRs
      secret-scanning: read    # opt back in to a category the baseline blocks
      actions/secrets: none    # deny a single subcategory outright
```

Set from the environment as a compact comma-separated list:

```bash theme={null}
export CYNATIVE_CONNECTORS_GITHUB_PERMISSIONS="default=read,issues=write,secret-scanning=none"
```

A non-empty environment value **replaces** the config-file map wholesale. A blank value is treated as unset. Duplicate keys are rejected at load time. When the ceiling is widened (`default=write`, an opened `secret-scanning`, or any `category:write` override), the startup inventory line is loud (`⚠`) and names exactly what was widened.

### Defense at a glance (GitHub)

| Control | Status |
| - | - |
| Read-only by default | ✓ (secret-scanning blocked even for reads) |
| Enforcement model | in-process per-request classification to a GitHub category/subcategory + access level, allowed only within the configured ceiling |
| Configurable exposure | ✓ · `connectors.github.permissions`: `category[/subcategory]` → `read\|write\|none` ceiling |
| Credential downscoping | — · user tokens can't be downscoped at runtime |
| Host pinning | ✓ (`api.github.com` + GitHub-owned download hosts, GET/HEAD) |
| Dial-time IP authorization | ✓ (public-internet guard — stricter than the default internal-range deny) |
| Model-supplied-credential rejection | ✓ |
| Response redaction | ✓ |

### GitHub limitations

* The connector supports `api.github.com` and the listed download hosts. [GitHub Enterprise Server](https://docs.github.com/en/enterprise-server@latest/admin/overview/about-github-enterprise-server) hosts are not supported.
* Actions-artifact downloads are not supported: GitHub redirects them to per-account Azure blob endpoints, which are outside the GitHub-owned host invariant.
* A configured `permissions` ceiling never *expands* the GitHub token's own scopes — the effective capability is the intersection of the two. Setting `issues: write` does nothing if the `gh` token cannot write issues.
* There is no client-side credential downscoping. A PAT, `gh`, or OAuth token has its scopes fixed at creation.
* The category routing table requires egress to `raw.githubusercontent.com`. With no network **and** no local cache, the connector fails closed — every `api.github.com` request is denied with a `github_hardening: category table not ready` error.

***

## GitLab connector

**Connector id:** `gitlab`\
**Default host:** `gitlab.com` (configurable for self-managed instances)

### Quick start

Authenticate with the GitLab CLI or export a Personal Access Token:

```bash theme={null}
glab auth login
cynative -p "review my GitLab projects for exposed secrets"
```

Or with an explicit token:

```bash theme={null}
export GITLAB_TOKEN=<read_api PAT>
cynative -p "list my GitLab merge requests"
```

No YAML is required for the default read-only mode against gitlab.com.

### Credential discovery

Cynative resolves a token for the configured host (default `gitlab.com`) in this order:

1. **Token environment variable** — `GITLAB_TOKEN`, then `GITLAB_ACCESS_TOKEN`, then `OAUTH_TOKEN` (glab's precedence order). The first non-empty value is used, exec-free, and takes precedence over any glab credential.
2. **glab (`glab auth credential-helper`)** — when no token env var is set, Cynative delegates to the pinned `glab` binary (requires **glab v1.85.2+**), which reads `config.yml` or the OS keyring, refreshes an expired OAuth token, and prints the credential as JSON.

Cynative reads these variables to discover the token and locate glab's config:

* `GITLAB_TOKEN` — first-precedence token.
* `GITLAB_ACCESS_TOKEN` — second-precedence token.
* `OAUTH_TOKEN` — third-precedence token.
* `GLAB_CONFIG_DIR` — exclusive override of glab's config directory.
* `XDG_CONFIG_HOME` — adds `$XDG_CONFIG_HOME/glab-cli/config.yml` as a config location.

If no token env var is set and glab has no usable credential, the connector is a quiet ambient skip. If a glab config exists but the `glab` binary is missing or too old, the connector is skipped **loudly** with a steer to install/upgrade glab or set `GITLAB_TOKEN`.

**Registration and validation:** When a token is discovered, Cynative validates it live at startup with a dial-guarded `GET /api/v4/user` before registering. An invalid, expired, or unreachable token shows the connector as unavailable with a reason rather than registering and failing every request.

**GitLab 18.9 or later is required** for self-managed instances (when using `api_reference`; the connector itself still registers but `api_reference` is unavailable for older instances).

**References:**

* [GitLab CLI (glab)](https://docs.gitlab.com/cli/) — `GITLAB_TOKEN`, `GLAB_CONFIG_DIR`; `glab auth login` and `glab auth credential-helper`.
* [GitLab personal access token scopes](https://docs.gitlab.com/user/profile/personal_access_tokens/) — `read_api` (least-privilege read), `api` (full), `read_repository`, `read_user`.

### Target selection

The GitLab connector targets GitLab SaaS — `https://gitlab.com`, REST API `/api/v4` — by default. The instance is chosen by Cynative config (`connectors.gitlab.host`, default `gitlab.com`) and host-pinned. The target is not selected per request; the URL must match the served host.

To point at a self-managed instance: set `connectors.gitlab.host: gitlab.example.com` (or `CYNATIVE_CONNECTORS_GITLAB_HOST`). Optionally set `connectors.gitlab.api_host` when API calls go to a different host than the login host, append `:port` for a non-443 instance, set `allow_private_network: true` for a private network, and `ca_cert` for a private CA.

### Request usage

Use `auth_provider: "gitlab"`. No connector-specific auth object is required.

```javascript theme={null}
const resp = await http_request({
  method: "GET",
  url: "https://gitlab.com/api/v4/projects",
  headers: [],
  body: "",
  auth_provider: "gitlab"
});
console.log(resp.body);
```

For self-managed instances, replace `gitlab.com` with your configured `host` (or `api_host`).

### Hardening

#### Host and port pinning

Host pinning allows only the configured `host` (default `gitlab.com`), or `api_host` when set. The TLS **port** is pinned too — the request URL's port must match the configured port (or 443 when none is configured). Redirects are never followed.

The connector serves the `/api/v4` REST API only. A request whose path is not under `/api/v4` is not category-classifiable and fails closed (`ErrUnclassifiable`).

#### Per-category exposure classification

The in-process classifier resolves each REST request to its GitLab category and required access level (read or write), then allows it only when the configured `connectors.gitlab.permissions` ceiling permits that level for that category.

* The check runs **before** the token is attached.
* Reads are `GET`, `HEAD`, `OPTIONS` (plus the `POST /api/v4/markdown` documentation-render endpoint); everything else is a write.
* The `ci-variables` category is denied by the secure baseline (even for reads) because its read endpoints return plaintext CI/CD variable values. Any OpenAPI template whose path contains a `variables` segment is mapped to the `ci-variables` category at table-distillation time.
* The GraphQL API (`/api/graphql`) is not supported and is always denied.
* A path with a `.` or `..` segment, raw or percent-encoded, is denied.

#### Model-supplied-credential rejection

The connector is the sole setter of credential material. The following are rejected: `Private-Token`, `Job-Token`, `Deploy-Token`, `X-Gitlab-Static-Object-Token`, `Authorization`, `Proxy-Authorization`, `X-Ms-Authorization-Auxiliary`, and `Cookie` headers; URL userinfo; the GitLab request-parameter credentials `token`, `private_token`, `access_token`, `job_token` (in query string, urlencoded, multipart, or JSON body); the `feed_token` / `rss_token` feed credentials; and the `sudo` impersonation control. After all checks pass, Cynative injects `Authorization: Bearer <token>`.

#### Dial-time IP authorization

By default, internal IP ranges (RFC1918, loopback, link-local including cloud metadata, ULA, IPv4-mapped forms) are denied. Set `allow_private_network: true` for self-managed GitLab instances on private networks. An unconditional floor remains denied even with the opt-in: loopback, link-local, and **all private (ULA) IPv6 (`fc00::/7`)** — ULA IPv6 stays denied because every major cloud parks its IPv6 instance-metadata service there.

#### CA certificate (self-managed with a private CA)

Set `connectors.gitlab.ca_cert` to the path of a PEM CA certificate file. `skip_tls_verify` is **not supported** — TLS validation cannot be bypassed.

### Cynative configuration

#### Target and connection settings

```yaml theme={null}
connectors:
  gitlab:
    host: gitlab.com              # optional; default is gitlab.com
    api_host: ""                  # optional; overrides the host used for API requests
    allow_private_network: false  # set true for self-managed instances on private networks
    ca_cert: ""                   # path to a PEM CA file for private-CA instances
```

```bash theme={null}
export CYNATIVE_CONNECTORS_GITLAB_HOST=gitlab.example.com
export CYNATIVE_CONNECTORS_GITLAB_API_HOST=api.gitlab.example.com
export CYNATIVE_CONNECTORS_GITLAB_ALLOW_PRIVATE_NETWORK=false
export CYNATIVE_CONNECTORS_GITLAB_CA_CERT=/path/to/ca.pem
```

`host` is the login host (the key glab uses in `config.yml`). Append `:port` to either `host` or `api_host` for a non-443 instance (e.g. `host: gitlab.internal:8443`).

#### Exposure and authorization

`connectors.gitlab.permissions` is a `category → read|write|none` ceiling. Omit it entirely for the secure default (read everything except `ci-variables`). `write` implies `read`; `none` denies even reads.

```yaml theme={null}
connectors:
  gitlab:
    permissions:
      default: read             # baseline ceiling for every category
      projects: write           # widen a single category to write
      ci-variables: read        # opt back in to a category the baseline blocks
```

```bash theme={null}
export CYNATIVE_CONNECTORS_GITLAB_PERMISSIONS="default=read,projects=write,ci-variables=none"
```

A non-empty environment value **replaces** the config-file map wholesale. A blank value is treated as unset. When the ceiling is widened (`default=write`, an opened `ci-variables`, or any `category:write` override), the startup inventory line is loud (`⚠`).

### Defense at a glance (GitLab)

| Control | Status |
| - | - |
| Read-only by default | ✓ |
| Enforcement model | in-process per-request classification to a GitLab category + access level, allowed only within the configured ceiling (GraphQL always denied) |
| Configurable exposure | ✓ · `connectors.gitlab.permissions`: `category` → `read\|write\|none` ceiling; default read-only, `ci-variables` blocked |
| Credential downscoping | — · GitLab has no runtime credential downscoping; token scopes are fixed at issue |
| Host pinning | ✓ (configured `host` / `api_host`; one instance per run; port pinned) |
| Dial-time IP authorization | ✓ (default internal-range deny; `allow_private_network: true` opt-in for private networks; loopback and link-local always denied) |
| Model-supplied-credential rejection | ✓ (incl. `Private-Token`, `Job-Token`, URL userinfo, body `token` field) |
| Response redaction | ✓ |

### GitLab limitations

* **CI job tokens** (endpoint-restricted) are out of scope. Cynative authenticates via `Authorization: Bearer`, which accepts PATs, project/group access tokens, and OAuth tokens.
* **The category routing table needs egress to `gitlab.com`.** With no network **and** no local cache the connector fails closed.
* **Container Registry and Pages** use separate hosts and authentication paths not covered by this connector.
* **Subpath-mounted installations** (e.g. `https://example.com/gitlab/`) are not supported.
* **A glab credential requires the `glab` binary** (v1.85.2+) on `PATH`. Set `GITLAB_TOKEN` for a fully exec-free path.
* **One instance per run.** Cynative registers at most one GitLab provider per session.
* **No runtime credential downscoping.** The token's scopes are fixed at issue time. The `permissions` ceiling narrows what Cynative will attempt but never expands the token's own scopes.
* **Private (ULA) IPv6 instances are unsupported** even with `allow_private_network: true`. Front a self-managed instance with an RFC1918 IPv4 or global-unicast IPv6 address instead.
* **A literal `token` field in a request body is rejected.** GitLab reads `params[:token]` (the pipeline-trigger credential) from any body encoding, so any model-supplied `token` field fails closed. Use the Bearer-authenticated `POST /projects/:id/pipeline` endpoint instead of the trigger endpoint.
* **`skip_tls_verify` is not supported.** Use `ca_cert` for self-managed instances with a private CA.

***

## Combined YAML configuration

A full configuration showing both connectors:

```yaml theme={null}
connectors:
  github:
    permissions:
      default: read
      secret-scanning: none    # explicitly block (this is already the default)
      issues: write            # example: allow creating issues
  gitlab:
    host: gitlab.com           # or your self-managed host
    api_host: ""               # optional separate API host
    allow_private_network: false
    ca_cert: ""
    permissions:
      default: read
      ci-variables: none       # explicitly block (this is already the default)
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.