# vuln-scan

A dependency vulnerability scanner for Pull Requests using **GitHub Dependabot alerts** and the **GitHub Advisory Database**.

It analyzes dependencies in a PR workspace and determines whether vulnerabilities are:

* ✅ Fixed in the PR
* ❌ Still vulnerable
* ⚠️ Newly introduced

Optionally, it can **comment on the PR and update commit status** to block merges based on configurable policy rules.

---

## 🚀 Features

* 🔍 Detect vulnerabilities via **Dependabot alerts (GraphQL)**
* 📦 Scan dependency manifests in the **current workspace**
* 🔁 Compare dependencies against the **default branch**
* 🧠 Evaluate vulnerabilities using **ecosystem-aware handlers**
* ⏱️ Configurable **grace periods by severity**
* 🧩 Extensible **multi-ecosystem architecture**
* 💬 Optional **PR comments + commit status updates**
* 🖥️ CLI-friendly output (**Rich / PrettyTable**)
* ⚡ Supports both **alerts + advisory-based evaluation**
* 🐳 Docker and Docker Compose support

---

## 📸 Example Output

```
                                                                                Dependency Vulnerabilities
┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ File                  ┃ Package                 ┃ Version ┃ Vulnerable Range    ┃ Patch Version ┃ Severity ┃ Blocking ┃ Reason                                                         ┃
┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ composer.lock         │ symfony/http-foundation │ 4.4.49  │ < 5.4.50            │ 5.4.50        │ HIGH     │ YES      │ HIGH vulnerability exceeded 14-day grace period                │
│ composer.lock         │ phpseclib/phpseclib     │ 3.0.39  │ >= 3.0.0, <= 3.0.49 │ 3.0.50        │ HIGH     │ NO       │ HIGH vulnerability within grace period (6 days remaining)      │
│ frontend/package.json │ handlebars              │ 4.7.7   │ >= 4.0.0, < 4.7.9   │ 4.7.9         │ MODERATE │ NO       │ MODERATE vulnerability within grace period (29 days remaining) │
│ composer.lock         │ firebase/php-jwt        │ 6.4.0   │ < 7.0.0             │ 7.0.0         │ LOW      │ NO       │ LOW vulnerability within grace period (22 days remaining)      │
└───────────────────────┴─────────────────────────┴─────────┴─────────────────────┴───────────────┴──────────┴──────────┴────────────────────────────────────────────────────────────────┘
Total findings: 4 (1 blocking, 3 non-blocking)
Dependabot alerts: https://github.com/theorchard/orchard/security/dependabot
```

The table includes:
- **File** — manifest or lockfile where the dependency was found
- **Package** — name of the vulnerable dependency
- **Version** — currently installed version
- **Vulnerable Range** — version range affected by the vulnerability
- **Patch Version** — minimum version that fixes the vulnerability
- **Severity** — CRITICAL, HIGH, MODERATE, or LOW
- **Blocking** — YES if the grace period has expired, NO if still within grace
- **Reason** — explanation of the blocking decision

---

## 📦 Installation

Requires **Python 3.13+**

```bash
git clone https://github.com/<org>/vuln-scan.git
cd vuln-scan
pip install -e .
```

Install development dependencies:

```bash
pip install -e .[dev]
```

---

## 🐳 Docker

### Build

```bash
docker build --target deploy -t vuln-scan .
```

### Run

```bash
docker run --rm \
  -e GITHUB_TOKEN="$GITHUB_TOKEN" \
  -e GITHUB_REPO_OWNER="theorchard" \
  -e GITHUB_REPO="lambda-abacus" \
  -e BASE_PATH="/workspace" \
  -e PROJECT_DIR="lambda/snapshot_contracts" \
  -v /path/to/your/repo:/workspace:ro \
  vuln-scan
```

With a `.env` file:

```bash
docker run --rm \
  --env-file .env \
  -v /path/to/your/repo:/workspace:ro \
  vuln-scan
```

Pass extra CLI flags after the image name:

```bash
docker run --rm \
  --env-file .env \
  -v /path/to/your/repo:/workspace:ro \
  vuln-scan --ux table --excluded-path vagrant
```

### Build Targets

| Target | Purpose |
|---|---|
| `deploy` | Production CLI image |
| `lint-and-test` | Runs `ruff check`, `mypy`, and `pytest` |

```bash
docker build --target lint-and-test -t vuln-scan-test .
docker run --rm vuln-scan-test
```

---

## 🐳 Docker Compose

### Run the scanner

```bash
docker compose run --rm --build cli
```

### Run (GitHub App auth)

```bash
docker run --rm \
  -e GITHUB_APP_ID="$GITHUB_APP_ID" \
  -e GITHUB_INSTALLATION_ID="$GITHUB_INSTALLATION_ID" \
  -e GITHUB_PRIVATE_KEY_PATH="/run/secrets/github_app_private_key.pem" \
  -e GITHUB_REPO_OWNER="theorchard" \
  -e GITHUB_REPO="lambda-abacus" \
  -e BASE_PATH="/workspace" \
  -v /path/to/your/repo:/workspace:ro \
  -v /path/to/private-key.pem:/run/secrets/github_app_private_key.pem:ro \
  vuln-scan
```

### Run linting and tests

```bash
docker compose run --rm --build lint-and-test
```

### Override volume mount inline

```bash
docker compose run --rm --build -v /path/to/your/repo:/workspace:ro cli
```

### Pass extra CLI flags

```bash
docker compose run --rm --build cli --ux table --excluded-path vagrant
```

---

## ⚙️ Configuration

Supports environment variables and `.env` files via `python-dotenv`.

Copy the shadow file to get started:

```bash
cp .env.shadow .env
# Edit .env with your values
```

> **Important:** Never commit `.env` — it is `.gitignore`d. Use `.env.shadow` as the reference template.

### Environment Variables

| Variable | CLI Flag | Default | Description |
|---|---|---|---|
| `GITHUB_TOKEN` | `--github-token` | — | GitHub personal access token (**required unless using GitHub App auth**) |
| `GITHUB_APP_ID` | `--github-app-id` | — | GitHub App ID (required for GitHub App auth) |
| `GITHUB_PRIVATE_KEY` | `--github-private-key` | — | GitHub App private key **contents** (alternative to `GITHUB_PRIVATE_KEY_PATH`) |
| `GITHUB_PRIVATE_KEY_PATH` | `--github-private-key-path` | — | Path to GitHub App private key `.pem` file |
| `GITHUB_INSTALLATION_ID` | `--github-installation-id` | — | GitHub App installation ID (required for GitHub App auth) |
| `GITHUB_REPO_OWNER` | `--owner` | `theorchard` | Repository owner |
| `GITHUB_REPO` | `--repo` | — | Repository name **(required)** |
| `BASE_PATH` | `--base-path` | — | Path to scan **(required)** |
| `PROJECT_DIR` | `--project-dir` | — | Subdirectory within base path |
| `GITHUB_PR_NUM` | `--pr-number` | — | PR number (required with `--post-comment`) |
| `COMMIT_SHA` | `--commit-sha` | — | Commit SHA (required with `--post-comment`) |
| `BUILD_URL` | `--build-url` | — | CI build URL (required with `--post-comment`) |
| `GITHUB_PR_COMMENT` | `--pr-comment-text` | `""` | PR comment text (for skip phrase detection) |
| `SKIP_PHRASE` | `--skip-phrase` | `skip vulnerability scan` | Phrase in PR comment to skip scan |
| `EXCLUDED_PATH` | `--excluded-path` | `vagrant` | Comma-separated paths to exclude from scanning |
| `CRITICAL_ERROR_DAYS` | `--grace-critical-days` | `7` | Grace period for critical vulnerabilities |
| `HIGH_ERROR_DAYS` | `--grace-high-days` | `14` | Grace period for high vulnerabilities |
| `MODERATE_ERROR_DAYS` | `--grace-moderate-days` | `30` | Grace period for moderate vulnerabilities |
| `LOW_ERROR_DAYS` | `--grace-low-days` | `60` | Grace period for low vulnerabilities |
| `NON_BLOCKING` | `--grace-non-blocking-days` | `90` | Non-blocking grace period |
| `MAX_WORKERS` | `--max-workers` | `4` | Global cap for parallel GitHub API tasks. Must be >= 1. |
| `LOG_LEVEL` | `--log-level` | `info` | Logging level: `debug`, `info`, `warning`, `error`, `critical` |
| — | `--ux` | `rich` (local) / `table` (`--post-comment`) | Output format: `rich`, `table`, `markdown` |
| — | `--post-comment` | `false` | Post PR comment and set commit status on GitHub |

> **Note:** When `--post-comment` is used, `--ux` defaults to `table`. Only `markdown` and `table` are allowed with `--post-comment`; `--ux rich` will be rejected.
---

### GitHub App Authentication (recommended for CI)

Instead of a Personal Access Token, you can authenticate using a GitHub App installation token.

Set:

- `GITHUB_APP_ID`
- `GITHUB_INSTALLATION_ID`
- and either:
  - `GITHUB_PRIVATE_KEY` (private key contents), or
  - `GITHUB_PRIVATE_KEY_PATH` (path to the `.pem` file)

The scanner will use GitHub App auth only when all of the above required values are provided (`GITHUB_APP_ID`, `GITHUB_INSTALLATION_ID`, and either `GITHUB_PRIVATE_KEY` or `GITHUB_PRIVATE_KEY_PATH`); otherwise it falls back to `GITHUB_TOKEN`.

## 🧪 Usage

### ▶️ Local Scan

```bash
vuln-scan \
  --owner example \
  --repo myrepo \
  --base-path .
```

What happens:

* Fetches open Dependabot alerts
* Scans local dependency manifests
* Matches dependencies against vulnerabilities

---

### 🤖 CI Usage (PR Mode)

```bash
vuln-scan \
  --owner $GITHUB_REPO_OWNER \
  --repo $GITHUB_REPO \
  --base-path $WORKSPACE \
  --pr-number $GITHUB_PR_NUM \
  --commit-sha $COMMIT_SHA \
  --build-url $BUILD_URL \
  --post-comment
```

When enabled:

* 💬 Posts a PR comment with findings (only when blocking vulnerabilities are found)
* 🚦 Updates commit status (success/failure)

> **Note:** When `--post-comment` is enabled, the reporter is automatically set to `table` since Rich output is not compatible with GitHub PR comments.

---

## 🧠 How It Works

```
CLI
 │
 ▼
VulnerabilityScanner
 │
 ├── WorkspaceScanner        (scan PR workspace)
 ├── DefaultBranchScanner    (scan base branch)
 ├── AlertsClient            (Dependabot alerts)
 ├── AdvisoryClient          (GitHub advisories)
 │
 ▼
Policy Engine (grace periods + blocking logic)
 │
 ▼
Reporter (Rich / Table)
 │
 ▼
Publisher (PR comment + status OR no-op)
```

---

## 🔐 Blocking Policy

Blocking decisions follow a priority-based evaluation:

### 1. Removed Dependencies
If the vulnerable dependency has been **removed** from the manifest → ✅ **non-blocking**

### 2. No Patch Available
If no patched version has been released yet → ✅ **non-blocking** (no action possible)

### 3. Already Fixed
If the installed version is **outside the vulnerable range** → ✅ **non-blocking**

### 4. Newly Introduced Dependencies
If a vulnerable dependency is **newly introduced** in the PR and a patch exists → ❌ **blocks immediately** (no grace period)

### 5. Existing Dependencies — Grace Period

For pre-existing dependencies that are vulnerable and have a patch available, blocking is based on **severity + age of vulnerability**:

| Severity | Grace Period |
| -------- | ------------ |
| Critical | 7 days       |
| High     | 14 days      |
| Moderate | 30 days      |
| Low      | 60 days      |

After the grace period expires → ❌ **build fails**

> **Note:** If no patch is available, a longer non-blocking grace period of 90 days is applied instead.

---

## 🧩 Supported Ecosystems

Currently:

* ✅ Python (`*requirements*.txt`, `pyproject.toml`, `setup.py`, `poetry.lock`, `Pipfile.lock`, `uv.lock`)
* ✅ Node.js (`package.json`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`)
* ✅ PHP (`composer.json`, `composer.lock`)
* ✅ Java (`pom.xml`)
* ✅ Ruby (`*.gemspec`, `Gemfile`, `Gemfile.lock`)
* ✅ Rust (`Cargo.toml`, `Cargo.lock`)
* ✅ Go (`go.mod`, `go.sum`)
* ✅ .NET (`packages.config`, `Directory.Packages.props`, `packages.lock.json`, `*.csproj`)

---

## 📁 Project Structure

```
src/vuln_scan
│
├── cli.py                # CLI entrypoint
├── core/                # Models, scanner, policy
├── ecosystems/          # Ecosystem handlers
├── github/              # GitHub API clients + publisher
├── io/                  # Workspace + repo scanning
├── reporting/           # Output renderers
└── utils/               # Logging, helpers
```

---

## 🧠 Design Principles

* **Separation of concerns** (scanner, policy, reporting, publishing)
* **Extensible ecosystem handlers**
* **Strong typing (mypy strict)**
* **Testable pipeline architecture**

---

## 🔚 Exit Codes

| Code | Meaning                        |
| ---- | ------------------------------ |
| 0    | No blocking vulnerabilities    |
| 1    | Blocking vulnerabilities found |

---

## 🛠 Development

Run tests:

```bash
pytest
```

Lint:

```bash
ruff check .
```

Format:

```bash
black .
```

Type check:

```bash
mypy src/
```

Using Docker:

```bash
docker compose run --rm --build lint-and-test
```

Format:

```bash
docker compose run --rm --build format
```

---
